avatar
threadsinstagram

NPM、Browserify、Webpack、ESM

Table of Contents

前言

關於 JavaScript 模組化的歷史不小心扯得有點太多,今天期待能簡潔做一些收尾,快快進到此系列的核心主題!

NPM (2009)

前面在 CommonJS 的章節中有稍微提過 Node.js 的歷史,這裡可以簡單根據 Node.js 紀錄片中對 NPM 作者 —— Isaac Schlueter 的訪談,記錄一些可以了解下的 fun fact。

在 Node.js 出現前,當時任職於 Yahoo 的 Isaac 和同事們就曾嘗試將 JavaScript 移植到 server side。儘管他一直知道有一個名為 Node.js 的新技術,也嘗試過最初的 v0.0.2 版,但並不滿意。直到後來發展到 v0.0.6 版時,在社群的推薦下,Isaac 實作拿 Node.js 開發後發現跟他的想像一致,因此也決定加入社群一起開發。

而當年他們有個線上討論群組,如果要去發佈與使用別人做好的套件其實很陽春,大概會像這樣的步驟:

  • 將套件丟到 GitHub、雲端位置或寄信附檔
  • 要使用或測試的人去下載一包 zip 檔
  • 解壓縮後複製到自己的 repo 裡
  • 根據文件自己去編譯出執行檔
  • 引入並測試、繼續開發

但這樣的流程似乎不夠聰明,也因此他參考其他語言的套件管理工具的方式,創造了 NPM 這個偉大的發明,讓後人在分享與使用各種 JavaScript 套件都能無痛引入。

Browserify (2011)

Browserify

昨天提到許多瀏覽器的模組化標準正爭執的如火如荼,而與此同時 James Halliday (substack) 這位老兄有另一種想法,NPM 的這些套件這麼方便,瀏覽器沒辦法用實在太可惜了,不如就像魔法師一般,寫個工具來讓瀏覽器也可以用 CommonJS 的方式載入現成的模組吧,因此 Browserify 就誕生了!

參考個官網上的 hello world 範例,當你用 npm install uniq 安裝了一個模組後,可以在你的前端程式中寫像是 CommonJS 的程式:

// main.js
var unique = require('uniq');
 
var data = [1, 2, 2, 3, 4, 5, 5, 5, 6];
 
console.log(unique(data));

但如果你今天直接在 HTML 載入這個 main.js 瀏覽器會認不得 require 的語法,因此你可以用 Browserify 做轉譯:

browserify main.js -o bundle.js

轉譯後你可以得到這樣的結果 (經過簡化),即可讓瀏覽器去順利執行:

// bundle.js
(function(){
  ...
})()({
  1: [function(require, module, exports) {
    // uniq 模組相關程式碼
    // ...
    module.exports = unique;
  }, {}],
  2: [function(require, module, exports) {
    // main.js
    var unique = require('uniq');
 
    var data = [1, 2, 2, 3, 4, 5, 5, 5, 6];
    console.log(unique(data));
  }, {"uniq": 1}]
}, {}, [2]);

Browserify 的運作原理可以這樣理解,可以看得到後續其他 bundler 的影子:

  • 從 entry point 的 JavaScript 檔案開始解析
  • 將程式碼轉成 AST (抽象語法樹) 如下圖
  • 遍歷 AST 去確認所有的 require 載入模組的地方
  • 遞迴地分析依賴模組中層層的關係,找出依賴關係圖
  • 最後將上述的依賴關係做打包並輸出
AST

另外也好奇查了下作者 James Halliday,目前幾乎已經沒什麼資料,連 GitHub 帳號都更換了,有查到在講 Browserify 的議程只有在 LXJS 2013 這一場,可以看到講話風格跟投影片都很 geek,還硬是酸了一下當時風風雨雨的 Node.js

LXJS 2013 - James Halliday

Webpack 的橫空出世 (2012)

參考這份簡報資料,當年的 Webpack 作者 Tobias Koppers 正在寫碩論,其中有個部份是關於網頁應用的優化,當時他需要找一個適合的 bundler,找到一個叫做 modules-webmake 的專案,但其中並沒有做 code splitting,因此他有向作者開了一個 issue 但沒有被重視,因此他決定自己弄髒手來做一個符合他需求的工具,而這就是 Webpack 的起源。

webpack issue from Tobias Koppers and response from modules-webmake author

不久後他完成了這個可以做程式分塊的版本,一開始取名叫 modules-webpack 想跟原套件打對台,而在此之後才更名叫做 Webpack。

在發佈後也漸漸地有其他開發者加入貢獻,陸續補上了各種 CommonJS、AMD 等模組化標準支援、各種廣義模組的 loader (style-loader、css-loader 等)、plugin system、compiler、HMR,直到 2014.02 終於釋出正式版 v1.0.0。

另外可能也多少受到 Browserify 啟發,以及其他當時的 task runner 如 Gulp.jsGrunt 跟著發跡,後來 Webpack 也成為了這些工具的集大成者,一直到今日除了作為 bundler 之外,也有完善的 loader 與 plugin 系統、CLI 工具、HMR dev server、code splitting、tree-shaking、支持多種模組化標準等,成為複雜的現代網頁開發中一個利器。

ESM 的一統天下 (2015)

最後也簡單提一下 ESM,其實模組化標準的最終結局的故事大家可能聽過很多了,在 ES6 終於定義了官方的模組化標準後,終於補上 JavaScript 最重要的一塊拼圖,讓後續如非同步加載、動態載入、tree-shaking 的靜態分析等有了穩健的基礎。

只是因為曾經有百家爭鳴的時代,也有 Node.js CommonJS 的流行,因此儘管 ESM 已經出道快十年,目前開發者仍偶爾需要注意多種模組化標準無法兼容的問題。

除了將 Babel 設定好做轉譯外,時至今日許多新工具與新版本仍在朝向統一標準努力,像是:

  • 像是 Node.js 多年來容易讓大家混淆的 .cjs.mjstype: ‘module’ 等等設定,也在今年的 v22 中提到可以用 --experimental-require-module 這個 flag 來幫助漸進式將現有的 CommonJS 遷移至 ESM。詳細說明也可以在參考 C.T 這篇《一篇文搞清楚 Node.js 模組行為,自由運用 CommonJS 與 ESM 模組》,這邊就不贅述
  • 去年強勢登場的 JavaScript runtime — Bun,其中一個特色就是支援 CommonJS 與 ESM 語法在同一個檔案中 (ref)

小結

今天終於一口氣帶到 ESM,不得不說講模組化歷史真的是個大坑,但寫完也是有一點小小成就感,雖然稱不上完美,有許多語法及細節沒提到,但可能就像 Huli 在這篇文章中說的:

「寫作的時候,選擇什麼要講什麼不講也是一門技藝」

就像我原本也是滿腔熱血覺得應該可以再一路講個 Rollup、Parcel,但總覺得再考古下去好像要離 Rust 這個 prefix 越來越遠了,期待下一篇開始可以直接來玩點有趣的實作,我們明天見!

參考資料