摘要:比如,這種模擬并不符合某些邊緣情況。然而,使用,結果是在引導模塊時,將指向,與使用的行為一致。這將更容易完成代碼庫的過渡,并使成為可能。
原文作者:Johannes Ewald @Jhnnns
原文鏈接:The state of JavaScript modules
已獲原作者授權翻譯及發布
ESM, CJS, UMD, AMD?—?到底應該選擇哪一個?
最近 在 twitter 上有很多關于 ES Module 現狀的討論,尤其是在 Node.js 上,他們計劃引入新的文件擴展名 *.mjs。人們有足夠理由對此感到 擔憂和不確定,因為這個話題異常復雜,接下來會盡力闡述清楚問題。
來自遠古的恐懼大多數前端開發者應該還記得 Javascript 依賴管理的黑暗時期。那個時候,你需要把一個庫復制粘貼到 vendor 文件夾,然后作為一個全局變量引入,要自己去按次序組合所有東西,可能還要管理命名空間。
在過去的那些年,我們能深刻體會到公共模塊格式化和中央模塊管理的價值。
在今天,不管是發布還是使用一個庫都要容易得多,只需要使用 npm publish 和 npm install 命令就行。這就是人們會那么緊張兩種模塊系統兼容性問題的原因:他們不想失去已有的舒適區。
接下來我會解釋和總結現有實現的情況,以及為什么 Node 生態遷移到 ES Module(ESM)會那么難。在最后,總結這些變化對 webpack 使用者和模塊作者有什么影響。
現有實現目前,ESM 有三種方式的實現:
瀏覽器
webpack 以及類似的構建工具
Node(未完成,但可能在年底作為一個實驗功能)
為了更好地理解現在的討論,首先要知道 ES2015 包含兩種模式:
script 用于具有全局命名空間的常規腳本
module 用于具有明確導入和導出的模塊化代碼
如果你試圖在 script 標簽使用 import 或者 export 語句,會拋出一個 SyntaxError。這種語句在全局環境下沒有任何意義。另一方面,module 模式即意味著嚴格模式,禁止使用某些語言特性,比如 with 語句。因此,需要在腳本被解析和執行之前定義模式。
瀏覽器中的 ESM截至到 2017 年 5 月,所有主流瀏覽器都開始做了 ESM 的實現工作。不過,大部分仍處于在實驗性質。這里不會做詳細介紹,因為 Jake Archibald 已經寫了一篇很厲害的文章。
除了一些小的困難,在瀏覽器中實現起來非常容易,因為以前并沒有模塊系統。想要指定 module 模式,需要在 script 標簽添加 type="module" 屬性,如下所示:
在一個模塊中,現在只能使用有效的 URL 作為模塊標識符。模塊標識符是用于 require 或 import 其他模塊的字符串。為了確保未來兼容 CJS 模塊標識符,“純” 導入標識符(如 import "lodash")現在還不支持。模塊標識符必須是絕對 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/
同樣需要注意的是,一旦處在一個模塊中,每個導入也將被解析為 module,而且沒有辦法 import 一個 script。
ESM 與 webpack類似 webpack 這樣的構建工具通常會嘗試用 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 使用函數包裝器封裝模塊范圍和對象引用來模擬 ESM 實時綁定。每次編譯,還包括一個模塊運行時,負責引導和緩存模塊。此外,將模塊標識轉換為數字模塊 ID。這樣可以減少打包的大小和引導時間。
這是什么意思呢?我們來看看編譯輸出:
(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 行為
結果已經簡化并刪除了一些與此示例無關的代碼。你會發現,webpack 在 exports 對象上將所有 export 語句替換成 Object.defineProperty,并使用屬性訪問器替換對引入值的所有引用。還要注意每個 ESM 開始時的 "use strict" 指令,這是由 webpack 自動添加,在 ESM 中必須是嚴格模式。
這種實現只是模擬,因為它試圖模仿 ESM 和 CJS 的行為 -- 但不是與其完全保持一致。比如,這種模擬并不符合某些邊緣情況??聪旅孢@個模塊:
console.log(this);
如果你通過加上 babel-preset-es2015 的 Babel 來運行,結果是:
"use strict"; console.log(undefined);
從輸出結果可以看出,Babel 假設默認是 ESM,因為 module 模式即代表嚴格模式,在嚴格模式下會將 this 初始化為 undefined。
然而,使用 webpack,結果是:
(function(module, exports) { console.log(this); })
在引導模塊時,this 將指向 exports ,與 Node.js 使用的 CJS 行為一致。這是因為語法上不確定是 script 還是 module,解析器無法判斷該模塊是 ESM 還是 CJS。在不明確的時候,webpack 會模擬 CJS,因為它仍然是最受歡迎的模塊風格。
這種模擬其實已經包含了很多情況,因為模塊作者通常會避免這種代碼。然而,“很多情況”對于像 Node.js 這樣的平臺是不夠的,因為它需要保證所有有效的 JavaScript 代碼都能正常運行。
Node.js 中的 ESMNode.js 在執行 ESM 時遇到了麻煩,因為仍然需要支持 CJS,語法看起來相似,但運行時行為完全不同。Node.js 核心技術委員會(CTC)成員 James M Snell 撰寫了一篇很好的文章來解釋 CJS 與 ESM 之間的差異。
歸結起來,CJS 是一個動態模塊系統,ESM 是靜態模塊系統。
CJS允許動態同步 require()
導出僅在模塊執行后才知道
導出可以在模塊初始化后添加,替換和刪除
ESM只允許靜態同步 import
在模塊執行之前,導入和導出已經關聯
導入和導出是不可變的
由于 CJS 早于 ES2015,所以一直在 script 模式下解析,封裝通過使用函數包裝器實現。在 Node.js 中加載 CJS,實際上會執行與此類似的代碼:
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 模塊的簡單函數包裝
問題出現了,將兩個模塊系統集成到同一個運行時時,ESM 和 CJS 之間的循環依賴可能會迅速導致類似死鎖的情況。
而且,由于現有 CJS 模塊數量龐大,也不能直接放棄對 CJS 的支持。為了避免 Node.js 生態的中斷,有兩點已經很明顯:
現有的 CJS 代碼必須以相同的方式繼續工作
兩個模塊系統都必須同時且盡可能無縫地工作
目前的權衡2017 年 3 月,經過幾個月的討論,CTC 終于找到了一種解決問題的辦法。由于在 ES 規范和引擎不改變的情況下無法進行無縫集成,CTC 決定開始一些權衡之后的實現工作:
1.ESM 必須是 *.mjs 文件擴展名這是由于上面提及的模糊語法問題,無法通過解析來確切知曉 JavaScript 代碼是什么類型。為了 Node.js 向后兼容的目標,作者需要加入一種新模式。已經有關于各種替代品的討論,但使用不同文件擴展名是解決目前問題的最佳權衡。
2.CJS 只能異步導入 ESM import()Node.js 將異步加載 ESM,以便盡可能接近瀏覽器的行為。因此,同步的 require() 在 ESM 是不可能的,并且依賴于 ESM 的每個功能都需要異步:
const driverPromise = import("dbdriver"); exports.readFromDb = async (query) => { return (await driverPromise).read(query); };3. CJS 向 ESM 暴露一個不可變的默認導出
使用 Babel 或 Webpack,我們通常將 CJS 重構為 ESM,如下所示:
// CJS const { a, b } = require("c"); // ESM import { a, b } from "c";
再一次地,他們的語法看起來很相似,但忽略了 CJS 中沒有命名導出的事實。只有一個叫做 default 的導出,等同于在 CJS 模塊完成計算后一個不可變的 module.exports 。從技術上講,有可能將 module.exports 解構成命名導入,但這需要對標準作更大的變更。這就是現在 CTC 決定采取這種方式的原因。
4.模塊范圍的變量類似 module,require 以及 __filename 在 ESM 不存在Node.js 和瀏覽器會實現一些 ESM 的特性,但標準化過程仍在進行中。
鑒于將 CJS 和 ESM 集成到一個運行時的工程挑戰,CTC 在評估邊緣情況和權衡方面做了非常好的工作。比如使用不同的文件擴展名是就是一個很簡單的解決方案。
實際上,一個文件擴展名可以認為是一個二進制文件如何解釋的提示。如果一個 module 不是 script,我們應該使用不同的文件擴展名。其他工具(如 linter 或 IDE )也可以獲取相同信息。
當然,引入新的文件擴展名有成本,但是一旦服務器和其他應用程序確認 *.mjs 為JavaScript,我們很快就會忘記這個爭議。
將 * .mjs 作為 Node.js 的 Python 3?考慮到所有這些限制,人們可能會問,這種過渡將對現在的生態造成什么樣的損害。雖然 CTC 會努力解決問題,但社區如何采用這一點仍然存在很大不確定性。這種不確定性 被眾多知名的 NPM 模塊作者 再次強調,他們聲稱將不會在模塊中使用 *.mjs。
Python 3 is killing Python
很難預測社區如何反應,但是應該不會對現在的生態造成大破壞,甚至能看到從 CJS 平穩過渡到 ESM。主要有兩個原因:
1.與 CJS 嚴格向后兼容那些不喜歡 ESM 的模塊作者可以繼續使用 CJS,保證自己不被排擠出局。這樣他們自己的代碼不會受到采用 ESM 的影響,降低遷移到另一個運行時的可能性,讓 NPM 遷移到新生態變得容易。從 CJS 到 ESM 的重構給包維護者帶來額外工作,不能指望所有人都有時間。
2. CJS 在 ESM 中的無縫整合從 ESM 導入 CJS 模塊非常簡單。需要注意的是,CJS 僅導出一個默認值。一旦處于 ESM,甚至可能根本不會注意到依賴關系使用的模塊風格,尤其是與在 CJS 中使用 await import()相比。
由于 ESM 的這個優點以及其他有點,比如開箱即用的 tree shaking 和瀏覽器兼容性,預計在未來幾年內,我們可以看到向 ESM 的緩慢而穩定的過渡。CJS 的特性,如動態 require() 和猴子補丁導出,在 Node.js 社區一直是有爭議的,不比 ESM 帶來的好處。
這些對我來說意味著什么?因為最近這些事情,很容易對目前存在的所有選擇和限制感到困惑。在接下來,整理了開發人員面臨的典型問題以及我們的回答:
現在需要重構現有的代碼嗎?不需要。Node.js 才剛剛開始實現 ESM,仍然有大量的工作要做。James M Snell 預計至少還需要一年時間,還有很多變化的余地,所以現在重構是不安全的。
應該在新代碼中使用 ESM 嗎?如果你已經有或者打算使用像 webpack 這樣的構建工具,答案是肯定的。這將更容易完成代碼庫的過渡,并使 tree shaking 成為可能。但要小心:一旦 Node.js 支持原生 ESM,可能需要重構其中的一些部分。
如果你正在編寫一個庫,答案是也肯定的,你的模塊使用者將受益于 tree shaking。
如果你不想進行構建操作,或者正在編寫一個 Node.js 應用程序,還是用 CJS 吧。
現在應該使用 .mjs 嗎?不要這樣做,目前沒有什么好處,工具支持依然薄弱。建議一旦原生 ESM 支持登陸 Node.js,盡快開始遷移。記住,瀏覽器只關心 MIME 類型,而不是文件擴展名。
應該關心瀏覽器兼容性嗎?是的,需要在一定程度上關注這個問題。 不應該在導入語句中省略 .js 擴展名,因為瀏覽器需要完整的 URL,無法像 Node.js 這樣執行路徑查詢。同樣,應該避免 index.js 文件。不過,人們并不會很快在瀏覽器中使用 NPM 軟件包,因為仍然不能 bare 導入。
作為庫作者該怎么辦?用 ESM 編寫代碼,并使用 Rollup 或 Webpack 轉換成單個 CJS 模塊,然后在 package.json 將 main 字段指向此 CJS 包,并將 module 字段指向原始 ESM。如果還使用 ESM 之外的其他新語言功能,則應編譯成 ES5,并提供 CJS 和 ESM 的打包。這樣,你的庫用戶仍然可以從 tree shaking 獲利而無需對代碼進行轉換。
看一下這些完成 tree shaking 的模塊
總結關于 ES 模塊有很多不確定性。由于目前 Node.js 在實現上的權衡,開發人員擔心可能會破壞 Node.js 的生態。
這些還不會發生,有兩個原因:CJS 的嚴格的后向兼容和 CJS 在 ESM 中的無縫集成。
在 Node.js 發布原生 ESM 支持之前,應該仍然使用 Rollup 和 Webpack 等工具。它們在一定程度上模擬了 ESM 環境,但要注意它們不完全符合規范。此外,使用打包仍然是個很好的選擇,一旦可以在瀏覽器中使用 NPM 軟件包。
我們 webpack 團隊正在努力做一些工作,幫助開發者平穩過渡。為了這個目標,我們計劃在 Node.js 的 ESM 支持成熟后,模擬 Node.js 導入 CJS 的方式。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/83216.html
摘要:現狀年月日,主流的四大瀏覽器達成了共識并宣布的最小可行產品已經完成。更快的函數調用當前,在中調用函數比想象的要慢。直接操作目前,沒有任何方式能夠操作。這就導致了部分應用可能會因此而推遲發布時間。結束現如今已經相當快速。 本文是圖說 WebAssembly 系列文章的最后一篇。如果您還未閱讀之前的文章,建議您從第一篇入手。 現狀 2017 年 2 月 28 日,主流的四大瀏覽器達成了共識...
摘要:感謝王下邀月熊分享的前端每周清單,為方便大家閱讀,特整理一份索引。王下邀月熊大大也于年月日整理了自己的前端每周清單系列,并以年月為單位進行分類,具體內容看這里前端每周清單年度總結與盤點。 感謝 王下邀月熊_Chevalier 分享的前端每周清單,為方便大家閱讀,特整理一份索引。 王下邀月熊大大也于 2018 年 3 月 31 日整理了自己的前端每周清單系列,并以年/月為單位進行分類,具...
摘要:在本次受訪者中,也有的開發者表示主要使用框架。這不剛發布了三個月,就已進入了特性凍結階段。根據官方統計,有的開發人員使用進行單元測試,而的人使用。此外,與開發者有所不同,開發者更習慣使用。對于語言的使用,表示,多數人使用單個全局。 showImg(https://upload-images.jianshu.io/upload_images/13825820-feaee185c3c95b...
摘要:前端每周清單年度總結與盤點在過去的八個月中,我幾乎只做了兩件事,工作與整理前端每周清單。本文末尾我會附上清單線索來源與目前共期清單的地址,感謝每一位閱讀鼓勵過的朋友,希望你們能夠繼續支持未來的每周清單。 showImg(https://segmentfault.com/img/remote/1460000010890043); 前端每周清單年度總結與盤點 在過去的八個月中,我幾乎只做了...
閱讀 811·2023-04-25 20:18
閱讀 2092·2021-11-22 13:54
閱讀 2527·2021-09-26 09:55
閱讀 3857·2021-09-22 15:28
閱讀 2969·2021-09-03 10:34
閱讀 1710·2021-07-28 00:15
閱讀 1629·2019-08-30 14:25
閱讀 1281·2019-08-29 17:16