NPM、Browserify、Webpack、ESM
前言
關於 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)
昨天提到許多瀏覽器的模組化標準正爭執的如火如荼,而與此同時 James Halliday (substack) 這位老兄有另一種想法,NPM 的這些套件這麼方便,瀏覽器沒辦法用實在太可惜了,不如就像魔法師一般,寫個工具來讓瀏覽器也可以用 CommonJS 的方式載入現成的模組吧,因此 Browserify 就誕生了!
參考個官網上的 hello world 範例,當你用 npm install uniq
安裝了一個模組後,可以在你的前端程式中寫像是 CommonJS 的程式:
但如果你今天直接在 HTML 載入這個 main.js
瀏覽器會認不得 require
的語法,因此你可以用 Browserify
做轉譯:
轉譯後你可以得到這樣的結果 (經過簡化),即可讓瀏覽器去順利執行:
Browserify 的運作原理可以這樣理解,可以看得到後續其他 bundler 的影子:
- 從 entry point 的 JavaScript 檔案開始解析
- 將程式碼轉成 AST (抽象語法樹) 如下圖
- 遍歷 AST 去確認所有的
require
載入模組的地方 - 遞迴地分析依賴模組中層層的關係,找出依賴關係圖
- 最後將上述的依賴關係做打包並輸出
另外也好奇查了下作者 James Halliday,目前幾乎已經沒什麼資料,連 GitHub 帳號都更換了,有查到在講 Browserify 的議程只有在 LXJS 2013 這一場,可以看到講話風格跟投影片都很 geek,還硬是酸了一下當時風風雨雨的 Node.js
Webpack 的橫空出世 (2012)
參考這份簡報資料,當年的 Webpack 作者 Tobias Koppers 正在寫碩論,其中有個部份是關於網頁應用的優化,當時他需要找一個適合的 bundler,找到一個叫做 modules-webmake 的專案,但其中並沒有做 code splitting,因此他有向作者開了一個 issue 但沒有被重視,因此他決定自己弄髒手來做一個符合他需求的工具,而這就是 Webpack 的起源。
不久後他完成了這個可以做程式分塊的版本,一開始取名叫 modules-webpack
想跟原套件打對台,而在此之後才更名叫做 Webpack。
在發佈後也漸漸地有其他開發者加入貢獻,陸續補上了各種 CommonJS、AMD 等模組化標準支援、各種廣義模組的 loader (style-loader、css-loader 等)、plugin system、compiler、HMR,直到 2014.02 終於釋出正式版 v1.0.0。
另外可能也多少受到 Browserify 啟發,以及其他當時的 task runner 如 Gulp.js、Grunt 跟著發跡,後來 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
、.mjs
、type: ‘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 越來越遠了,期待下一篇開始可以直接來玩點有趣的實作,我們明天見!