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