JavaScript 模塊系統:一個長達二十年的誤判
每當深入 JS 模塊化相關的問題時,總會產生一種懷疑:是不是從一開始方向就錯了?
這不僅僅是 “導出方式寫錯了” 這種小問題,而是整個 JavaScript 模塊系統本身,就像是一個被修補了二十年的臨時方案 —— 直到今天,仍然一團亂麻。
那些沒有規則的日子
在 2000 年代初,JavaScript 還沒決定自己是門“真正的語言”,還是只是用來讓按鈕發光的工具。那時根本沒有模塊的概念,代碼共享的方式就是把所有函數、變量扔進全局作用域,然后祈禱別的庫沒有也定義一個叫 Utils
的東西。
混亂,毫無約束。但至少,所有人都知道規則是:沒有規則。
添加一個 <script>
標簽,雙手合十,期盼不要因為某個第三方庫覆蓋了 window
上的變量而整個頁面宕機。
“設計” 模塊的前傳:模式與變通
在沒有官方模塊系統的時代,開發者開始自救。
于是出現了 IIFE(立即執行函數表達式)、模塊暴露模式、命名空間對象……這些東西像極了用牙簽搭建家具 —— 不是不能用,就是極不穩定。
每個團隊都有自己一套“模塊化寫法”,彼此不兼容,但都自認為聰明無比。
Node.js:另一種錯誤的開始
隨后 Node.js 出現,帶來了 CommonJS 模塊規范。require
和 module.exports
變成主流。
這是 JS 首次擁有模塊系統 —— 但只是“看起來”像。
因為瀏覽器不支持,前端用不了,于是前端社區又開始發明各種工具:Browserify、Webpack……目的只有一個 —— 讓不兼容的模塊在瀏覽器里“看起來能用”。
于是,整個工具鏈的價值,變成了在語言不支持的前提下,強行“偽裝”支持模塊。
ES Module:遲來的“修復”,不是解決方案
2015 年,ES6(或稱 ES2015)終于發布了原生的模塊系統:import
/ export
。
聽起來像是個真正的解決方案?
太遲了。
此時整個生態早已分裂:
- Node 仍在用 CommonJS;
- 瀏覽器只能通過
<script type="module">
使用 ESM; - 構建工具需要配置打包、轉譯、偽裝;
.mjs
、.cjs
、.js
混在一起,構建過程仿佛踩地雷;- 動態導入、Top-level await、Tree shaking 兼容問題如影隨形。
這時的模塊系統,不再是語言特性,而是“歷史遺產的兼容適配層”。
JavaScript 的模塊:從未被“設計”過
JS 的模塊系統,不是設計出來的,而是一次次危機中的補丁。
語言本身從未為模塊準備過標準。社區只是不斷在漏洞上堆積抽象,一層壓一層。
到現在,大家勉強接受這種局面,只是因為太晚了,退無可退。
模塊系統背后的“開發者體驗稅”
“JavaScript 疲勞”成了一個時代的梗,其根源之一就是模塊系統的混亂。
- 想用一個包?希望它能同時支持 CommonJS 和 ESM;
- 想打包?希望 bundler 能識別
.mjs
不炸; - 想發布一個庫?先研究清楚
default
和named export
的互操作規則。
原本最基礎的“共享代碼”,現在變成最復雜的構建難題。
這早已不是“寫代碼”的問題,而是一個系統性負擔 —— 每一次導入函數,都可能觸發構建崩潰。
如果當初設計了模塊系統……
如果從一開始,JavaScript 就有原生模塊系統,即使是最基礎的版本,那么如今的一切將會大不相同:
- 可能不會出現如此龐大的構建工具生態;
- 不會有三四種模塊格式混雜在同一個項目中;
- 不會為了導入函數還要查 StackOverflow 判斷導出方式;
- 不會因為一個
.mjs
文件名導致整個打包失敗。
然而歷史沒有如果。一切“聰明”的補丁,如今都變成了沉重的負擔。
總結:問題太深,已經無法修復
模塊系統的問題,早已不是簡單的“設計優化”可以解決。
這已經是一個生態系統的集體妥協:
- 社區工具已經習慣了補丁方案;
- 龐大的歷史項目無法統一遷移;
- 新的開發者甚至以為“這就是正常的模塊使用方式”。
最終,所有人都在忍受一個沒人滿意、但無法徹底推翻的系統。
所以:
- 當你在控制臺看到
Cannot use import statement outside a module
; - 當你切換文件擴展名到
.mjs
卻導致構建工具崩潰; - 當你嘗試混用
require()
和import
報錯報到崩潰……
不必責怪自己。
這不是“寫錯了”,這是一場語言層級的、持續二十年的妥協后遺癥。
下次有人說 JavaScript 模塊系統“現代”、“優雅”,不妨反問一句:
“這不就是修了二十年的漏洞堆出來的嗎?”
如果對方沉默了幾秒,你就知道答案了。