JavaScript 模塊化現(xiàn)狀

原文鏈接:The state of JavaScript modules

ESM, CJS, UMD, AMD?—?到底應(yīng)該選擇哪一個母剥?

最近 在 twitter 上有很多關(guān)于 ES Module 現(xiàn)狀的討論环疼,尤其是在 Node.js 上炫隶,他們計劃引入新的文件擴展名 *.mjs伪阶。人們有足夠理由對此感到 擔憂和不確定栅贴,因為這個話題異常復(fù)雜,接下來會盡力闡述清楚問題凝赛。

來自遠古的恐懼

大多數(shù)前端開發(fā)者應(yīng)該還記得 Javascript 依賴管理的黑暗時期墓猎。那個時候陶衅,你需要把一個庫復(fù)制粘貼到 vendor 文件夾直晨,然后作為一個全局變量引入勇皇,要自己去按次序組合所有東西敛摘,可能還要管理命名空間兄淫。

在過去的那些年捕虽,我們能深刻體會到公共模塊格式化和中央模塊管理的價值泄私。

在今天晌端,不管是發(fā)布還是使用一個庫都要容易得多,只需要使用 npm publishnpm install 命令就行蓬痒。這就是人們會那么緊張兩種模塊系統(tǒng)兼容性問題的原因:他們不想失去已有的舒適區(qū)梧奢。

接下來我會解釋和總結(jié)現(xiàn)有實現(xiàn)的情況粹断,以及為什么 Node 生態(tài)遷移到 ES Module(ESM)會那么難瓶埋。在最后养筒,總結(jié)這些變化對 webpack 使用者和模塊作者有什么影響端姚。

現(xiàn)有實現(xiàn)

目前渐裸,ESM 有三種方式的實現(xiàn):

為了更好地理解現(xiàn)在的討論洞渤,首先要知道 ES2015 包含兩種模式:

  • script 用于具有全局命名空間的常規(guī)腳本
  • module 用于具有明確導(dǎo)入和導(dǎo)出的模塊化代碼

如果你試圖在 script 標簽使用 import 或者 export 語句载迄,會拋出一個 SyntaxError。這種語句在全局環(huán)境下沒有任何意義魂迄。另一方面极祸,module 模式即意味著嚴格模式,禁止使用某些語言特性浴捆,比如 with 語句选泻。因此页眯,需要在腳本被解析和執(zhí)行之前定義模式窝撵。

瀏覽器中的 ESM

截至到 2017 年 5 月碌奉,所有主流瀏覽器都開始做了 ESM 的實現(xiàn)工作赐劣。不過哩都,大部分仍處于在實驗性質(zhì)漠嵌。這里不會做詳細介紹儒鹿,因為 Jake Archibald 已經(jīng)寫了一篇很厲害的文章

除了一些小的困難,在瀏覽器中實現(xiàn)起來非常容易章钾,因為以前并沒有模塊系統(tǒng)贱傀。想要指定 module 模式伊脓,需要在 script 標簽添加 type="module" 屬性魁衙,如下所示:

<script type="module" src="main.js"></script>

在一個模塊中剖淀,現(xiàn)在只能使用有效的 URL 作為模塊標識符纵隔。模塊標識符是用于 require 或 import 其他模塊的字符串捌刮。為了確保未來兼容 CJS 模塊標識符绅作,“純” 導(dǎo)入標識符(如 import "lodash")現(xiàn)在還不支持蛾派。模塊標識符必須是絕對 URL 或者是以 /碍脏, ./, ../ 開頭:

// Supported:
import {foo} from 'https://jakearchibald.com/utils/bar.js';
import {foo} from '/utils/bar.js';
import {foo} from './bar.js';
import {foo} from '../bar.js';

// Not supported:
import {foo} from 'bar.js';
import {foo} from 'utils/bar.js';
// Example from https://jakearchibald.com/2017/es-modules-in-browsers/

同樣需要注意的是典尾,一旦處在一個模塊中,每個導(dǎo)入也將被解析為 module河闰,而且沒有辦法 import 一個 script姜性。

ESM 與 webpack

類似 webpack 這樣的構(gòu)建工具通常會嘗試用 module 模式解析代碼部念,有問題再切回到 script 模式儡炼。這些工具最終會生成一段 script乌询,通常是在一定程度上模擬 CJS 和 ESM 行為的模塊運行時妹田。

我們以這兩個簡單的 ESM 為例:

// a.js
export let number = 42;
export function incr() {
    number++;
}
// test.js
import { number } from "./a";

console.log(number);

webpack 使用函數(shù)包裝器封裝模塊范圍和對象引用來模擬 ESM 實時綁定。每次編譯驶拱,還包括一個模塊運行時屯烦,負責引導(dǎo)和緩存模塊驻龟。此外缸匪,將模塊標識轉(zhuǎn)換為數(shù)字模塊 ID。這樣可以減少打包的大小和引導(dǎo)時間露懒。

這是什么意思呢懈词?我們來看看編譯輸出:

(function(modules) {
    // This is the module runtime.
    // It's only included once per compilation.
    // Other chunks share the same runtime.
    var installedModules = {};
    // The require function
    function __webpack_require__(moduleId) {
        ...
    }
    ...
    // Load entry module and return exports
    return __webpack_require__(__webpack_require__.s = 1);
})
([ // An array that maps module ids to functions
    // a.js as module id 0
    function (module, __webpack_exports__, __webpack_require__) {
        "use strict";
        Object.defineProperty(__webpack_exports__, "a", {
            configurable: false,
            enumerable: true,
            get: () => number
        });

        let number = 42;

        function incr() {
            number++;
        }
    },
    // test.js as module id 1
    function (module, __webpack_exports__, __webpack_require__) {
        "use strict";
        var __WEBPACK_IMPORTED_MODULE_0__a__ = __webpack_require__(0);

        // Object reference as "live binding"
        console.log(__WEBPACK_IMPORTED_MODULE_0__a__["a" /* number */]);
    }
]);

簡化的 webpack 輸出,模擬 ES Modules 行為

結(jié)果已經(jīng)簡化并刪除了一些與此示例無關(guān)的代碼译暂。你會發(fā)現(xiàn),webpack 在 exports 對象上將所有 export 語句替換成 Object.defineProperty崎脉,并使用屬性訪問器替換對引入值的所有引用囚灼。還要注意每個 ESM 開始時的 "use strict" 指令祭衩,這是由 webpack 自動添加汪厨,在 ESM 中必須是嚴格模式劫乱。

這種實現(xiàn)只是模擬,因為它試圖模仿 ESM 和 CJS 的行為 -- 但不是與其完全保持一致狭吼。比如刁笙,這種模擬并不符合某些邊緣情況疲吸≌玻看下面這個模塊:

console.log(this);

如果你通過加上 babel-preset-es2015 的 Babel 來運行蹂喻,結(jié)果是:

"use strict";
console.log(undefined);

從輸出結(jié)果可以看出口四,Babel 假設(shè)默認是 ESM秦陋,因為 module 模式即代表嚴格模式,在嚴格模式下會將 this 初始化為 undefined粪小。

然而探膊,使用 webpack逞壁,結(jié)果是:

(function(module, exports) {

console.log(this);

})

在引導(dǎo)模塊時锐锣,this 將指向 exports 姿骏,與 Node.js 使用的 CJS 行為一致斤彼。這是因為語法上不確定是 script 還是 module,解析器無法判斷該模塊是 ESM 還是 CJS嘲玫。在不明確的時候跪另,webpack 會模擬 CJS触机,因為它仍然是最受歡迎的模塊風(fēng)格株婴。

這種模擬其實已經(jīng)包含了很多情況取视,因為模塊作者通常會避免這種代碼。然而蹋肮,“很多情況”對于像 Node.js 這樣的平臺是不夠的璧疗,因為它需要保證所有有效的 JavaScript 代碼都能正常運行漆魔。

Node.js 中的 ESM

Node.js 在執(zhí)行 ESM 時遇到了麻煩改抡,因為仍然需要支持 CJS系瓢,語法看起來相似阿纤,但運行時行為完全不同。Node.js 核心技術(shù)委員會(CTC)成員 James M Snell 撰寫了一篇很好的文章來解釋 CJS 與 ESM 之間的差異夷陋。

歸結(jié)起來欠拾,CJS 是一個動態(tài)模塊系統(tǒng),ESM 是靜態(tài)模塊系統(tǒng)骗绕。

CJS

  • 允許動態(tài)同步 require()
  • 導(dǎo)出僅在模塊執(zhí)行后才知道
  • 導(dǎo)出可以在模塊初始化后添加藐窄,替換和刪除

ESM

  • 只允許靜態(tài)同步 import
  • 在模塊執(zhí)行之前,導(dǎo)入和導(dǎo)出已經(jīng)關(guān)聯(lián)
  • 導(dǎo)入和導(dǎo)出是不可變的

由于 CJS 早于 ES2015酬土,所以一直在 script 模式下解析荆忍,封裝通過使用函數(shù)包裝器實現(xiàn)。在 Node.js 中加載 CJS,實際上會執(zhí)行與此類似的代碼:

const module = {
    exports: {}
};
const require = makeRequireFunction();
const filename = "...";
const dirname = "...";
(function (exports, require, module, __filename, __dirname) {
/* YOUR CODE */
})(module.exports, require, module, filename, dirname);

對 Node.js 的 CommonJS 模塊的簡單函數(shù)包裝

問題出現(xiàn)了凉袱,將兩個模塊系統(tǒng)集成到同一個運行時時,ESM 和 CJS 之間的循環(huán)依賴可能會迅速導(dǎo)致類似死鎖的情況棺耍。

而且害幅,由于現(xiàn)有 CJS 模塊數(shù)量龐大,也不能直接放棄對 CJS 的支持。為了避免 Node.js 生態(tài)的中斷记盒,有兩點已經(jīng)很明顯:

  • 現(xiàn)有的 CJS 代碼必須以相同的方式繼續(xù)工作
  • 兩個模塊系統(tǒng)都必須同時且盡可能無縫地工作

目前的權(quán)衡

2017 年 3 月彬碱,經(jīng)過幾個月的討論,CTC 終于找到了一種解決問題的辦法估盘。由于在 ES 規(guī)范和引擎不改變的情況下無法進行無縫集成箫踩,CTC 決定開始一些權(quán)衡之后的實現(xiàn)工作

1.ESM 必須是 *.mjs 文件擴展名

這是由于上面提及的模糊語法問題,無法通過解析來確切知曉 JavaScript 代碼是什么類型。為了 Node.js 向后兼容的目標,作者需要加入一種新模式猿规。已經(jīng)有關(guān)于各種替代品的討論师郑,但使用不同文件擴展名是解決目前問題的最佳權(quán)衡。

2.CJS 只能異步導(dǎo)入 ESM import()

Node.js 將異步加載 ESM,以便盡可能接近瀏覽器的行為洁闰。因此腰素,同步的 require() 在 ESM 是不可能的,并且依賴于 ESM 的每個功能都需要異步:

const driverPromise = import("dbdriver");

exports.readFromDb = async (query) => {
   return (await driverPromise).read(query);
};

3. CJS 向 ESM 暴露一個不可變的默認導(dǎo)出

使用 Babel 或 Webpack,我們通常將 CJS 重構(gòu)為 ESM扶歪,如下所示:

// CJS
const { a, b } = require("c");
// ESM
import { a, b } from "c";

再一次地炫欺,他們的語法看起來很相似摩桶,但忽略了 CJS 中沒有命名導(dǎo)出的事實辅斟。只有一個叫做 default 的導(dǎo)出蔗崎,等同于在 CJS 模塊完成計算后一個不可變的 module.exports 。從技術(shù)上講徘熔,有可能將 module.exports 解構(gòu)成命名導(dǎo)入讶凉,但這需要對標準作更大的變更褐望。這就是現(xiàn)在 CTC 決定采取這種方式的原因

4.模塊范圍的變量類似 modulerequire 以及 __filename 在 ESM 不存在

Node.js 和瀏覽器會實現(xiàn)一些 ESM 的特性女坑,但標準化過程仍在進行中盟广。

鑒于將 CJS 和 ESM 集成到一個運行時的工程挑戰(zhàn)桨武,CTC 在評估邊緣情況和權(quán)衡方面做了非常好的工作窿吩。比如使用不同的文件擴展名是就是一個很簡單的解決方案。

實際上扣猫,一個文件擴展名可以認為是一個二進制文件如何解釋的提示。如果一個 module 不是 script饰潜,我們應(yīng)該使用不同的文件擴展名半沽。其他工具(如 linter 或 IDE )也可以獲取相同信息幔托。

當然,引入新的文件擴展名有成本篇梭,但是一旦服務(wù)器和其他應(yīng)用程序確認 *.mjs 為JavaScript竣付,我們很快就會忘記這個爭議惹恃。

將 * .mjs 作為 Node.js 的 Python 3采蚀?

考慮到所有這些限制神妹,人們可能會問庸疾,這種過渡將對現(xiàn)在的生態(tài)造成什么樣的損害词渤。雖然 CTC 會努力解決問題塞淹,但社區(qū)如何采用這一點仍然存在很大不確定性匈挖。這種不確定性 被眾多知名的 NPM 模塊作者 再次強調(diào),他們聲稱將不會在模塊中使用 *.mjs缤灵。

Python 3 is killing Python

很難預(yù)測社區(qū)如何反應(yīng)腮出,但是應(yīng)該不會對現(xiàn)在的生態(tài)造成大破壞,甚至能看到從 CJS 平穩(wěn)過渡到 ESM。主要有兩個原因:

1.與 CJS 嚴格向后兼容

那些不喜歡 ESM 的模塊作者可以繼續(xù)使用 CJS垒迂,保證自己不被排擠出局乐导。這樣他們自己的代碼不會受到采用 ESM 的影響,降低遷移到另一個運行時的可能性压昼,讓 NPM 遷移到新生態(tài)變得容易。從 CJS 到 ESM 的重構(gòu)給包維護者帶來額外工作瘤运,不能指望所有人都有時間窍霞。

2. CJS 在 ESM 中的無縫整合

從 ESM 導(dǎo)入 CJS 模塊非常簡單。需要注意的是拯坟,CJS 僅導(dǎo)出一個默認值但金。一旦處于 ESM,甚至可能根本不會注意到依賴關(guān)系使用的模塊風(fēng)格似谁,尤其是與在 CJS 中使用 await import()相比傲绣。

由于 ESM 的這個優(yōu)點以及其他有點掠哥,比如開箱即用的 tree shaking 和瀏覽器兼容性巩踏,預(yù)計在未來幾年內(nèi),我們可以看到向 ESM 的緩慢而穩(wěn)定的過渡续搀。CJS 的特性塞琼,如動態(tài) require() 和猴子補丁導(dǎo)出,在 Node.js 社區(qū)一直是有爭議的禁舷,不比 ESM 帶來的好處彪杉。

這些對我來說意味著什么毅往?

因為最近這些事情,很容易對目前存在的所有選擇和限制感到困惑派近。在接下來攀唯,整理了開發(fā)人員面臨的典型問題以及我們的回答:

現(xiàn)在需要重構(gòu)現(xiàn)有的代碼嗎?

不需要渴丸。Node.js 才剛剛開始實現(xiàn) ESM侯嘀,仍然有大量的工作要做。James M Snell 預(yù)計至少還需要一年時間谱轨,還有很多變化的余地戒幔,所以現(xiàn)在重構(gòu)是不安全的。

應(yīng)該在新代碼中使用 ESM 嗎土童?

  • 如果你已經(jīng)有或者打算使用像 webpack 這樣的構(gòu)建工具诗茎,答案是肯定的。這將更容易完成代碼庫的過渡献汗,并使 tree shaking 成為可能敢订。但要小心:一旦 Node.js 支持原生 ESM,可能需要重構(gòu)其中的一些部分罢吃。
  • 如果你正在編寫一個庫枢析,答案是也肯定的,你的模塊使用者將受益于 tree shaking刃麸。
  • 如果你不想進行構(gòu)建操作醒叁,或者正在編寫一個 Node.js 應(yīng)用程序,還是用 CJS 吧泊业。

現(xiàn)在應(yīng)該使用 .mjs 嗎把沼?

不要這樣做,目前沒有什么好處吁伺,工具支持依然薄弱饮睬。建議一旦原生 ESM 支持登陸 Node.js,盡快開始遷移篮奄。記住捆愁,瀏覽器只關(guān)心 MIME 類型,而不是文件擴展名窟却。

應(yīng)該關(guān)心瀏覽器兼容性嗎昼丑?

是的,需要在一定程度上關(guān)注這個問題夸赫。 不應(yīng)該在導(dǎo)入語句中省略 .js 擴展名菩帝,因為瀏覽器需要完整的 URL,無法像 Node.js 這樣執(zhí)行路徑查詢。同樣呼奢,應(yīng)該避免 index.js 文件宜雀。不過,人們并不會很快在瀏覽器中使用 NPM 軟件包握础,因為仍然不能 bare 導(dǎo)入辐董。

作為庫作者該怎么辦?

用 ESM 編寫代碼禀综,并使用 Rollup 或 Webpack 轉(zhuǎn)換成單個 CJS 模塊郎哭,然后在 package.jsonmain 字段指向此 CJS 包,并將 module 字段指向原始 ESM菇存。如果還使用 ESM 之外的其他新語言功能夸研,則應(yīng)編譯成 ES5,并提供 CJS 和 ESM 的打包依鸥。這樣亥至,你的庫用戶仍然可以從 tree shaking 獲利而無需對代碼進行轉(zhuǎn)換。

看一下這些完成 tree shaking 的模塊

總結(jié)

關(guān)于 ES 模塊有很多不確定性贱迟。由于目前 Node.js 在實現(xiàn)上的權(quán)衡姐扮,開發(fā)人員擔心可能會破壞 Node.js 的生態(tài)。

這些還不會發(fā)生衣吠,有兩個原因:CJS 的嚴格的后向兼容和 CJS 在 ESM 中的無縫集成茶敏。

在 Node.js 發(fā)布原生 ESM 支持之前,應(yīng)該仍然使用 Rollup 和 Webpack 等工具缚俏。它們在一定程度上模擬了 ESM 環(huán)境惊搏,但要注意它們不完全符合規(guī)范。此外忧换,使用打包仍然是個很好的選擇恬惯,一旦可以在瀏覽器中使用 NPM 軟件包。

我們 webpack 團隊正在努力做一些工作亚茬,幫助開發(fā)者平穩(wěn)過渡酪耳。為了這個目標,我們計劃在 Node.js 的 ESM 支持成熟后刹缝,模擬 Node.js 導(dǎo)入 CJS 的方式碗暗。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市梢夯,隨后出現(xiàn)的幾起案子言疗,更是在濱河造成了極大的恐慌,老刑警劉巖厨疙,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件洲守,死亡現(xiàn)場離奇詭異疑务,居然都是意外死亡沾凄,警方通過查閱死者的電腦和手機梗醇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來撒蟀,“玉大人叙谨,你說我怎么就攤上這事”M停” “怎么了手负?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長姑尺。 經(jīng)常有香客問我竟终,道長,這世上最難降的妖魔是什么切蟋? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任统捶,我火速辦了婚禮,結(jié)果婚禮上柄粹,老公的妹妹穿的比我還像新娘喘鸟。我一直安慰自己,他們只是感情好驻右,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布什黑。 她就那樣靜靜地躺著,像睡著了一般堪夭。 火紅的嫁衣襯著肌膚如雪愕把。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天森爽,我揣著相機與錄音礼华,去河邊找鬼。 笑死拗秘,一個胖子當著我的面吹牛圣絮,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播雕旨,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼扮匠,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了凡涩?” 一聲冷哼從身側(cè)響起棒搜,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎活箕,沒想到半個月后力麸,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年克蚂,在試婚紗的時候發(fā)現(xiàn)自己被綠了闺鲸。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡埃叭,死狀恐怖摸恍,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情赤屋,我是刑警寧澤立镶,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站类早,受9級特大地震影響媚媒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜涩僻,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一缭召、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧令哟,春花似錦恼琼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至狠半,卻和暖如春噩死,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背神年。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工已维, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人已日。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓垛耳,卻偏偏與公主長得像,于是被迫代替她去往敵國和親飘千。 傳聞我的和親對象是個殘疾皇子堂鲜,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容