當前位置:
首頁 > 科技 > 【第957期】JavaScript 模塊現狀

【第957期】JavaScript 模塊現狀

前言

周末一篇《深度學習成了前端開發神器:根據UI設計圖自動生成代碼》刷遍朋友圈,紛紛表示在失業的路上了。今日早讀文章由 @ 冉余 翻譯授權分享。

正文從這開始~

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 模塊標識符,「bare」 導入標誌符(如 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

exportletnumber=42;

exportfunctionincr(){

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.

varinstalledModules={};

// 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

});

letnumber=42;

functionincr(){

number++;

}

},

// test.js as module id 1

function(module,__webpack_exports__,__webpack_require__){

"use strict";

var__WEBPACK_IMPORTED_MODULE_0__a__=__webpack_require__();

// 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 中的 ESM

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

歸結起來,CJS 是一個動態模塊系統,ESM 是靜態模塊系統。

CJS

允許動態同步 require()

導出僅在模塊執行後才知道

導出可以在模塊初始化後添加,替換和刪除

ESM

只允許靜態同步 import

在模塊執行之前,導入和導出已經關聯

導入和導出是不可變的

由於 CJS 早於 ES2015,所以一直在 script 模式下解析,封裝通過使用函數包裝器實現。在 Node.js 中載入 CJS,實際上會執行與此類似的代碼:

constmodule={

exports:{}

};

constrequire=makeRequireFunction();

constfilename="...";

constdirname="...";

(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 的每個功能都需要非同步:

constdriverPromise=import("dbdriver");

exports.readFromDb=async(query)=>{

return(awaitdriverPromise).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 的方式。

關於本文

譯者:@冉余

作者:@Johannes Ewald @Jhnnns

原文:https://medium.com/webpack/the-state-of-javascript-modules-4636d1774358

點擊展開全文

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 前端早讀課 的精彩文章:

「零廣告,全乾貨」iWeb峰會上海站,最後500位參會名額限免來襲!
代碼審查應該關注什麼:數據結構
我對知乎前端相關問題的十問十答
你不懂JS:ES6與未來之元編程(下)
你不懂JS:ES6與未來之元編程(中)

TAG:前端早讀課 |

您可能感興趣

wonder girls成員現狀:29歲閔先藝近照顯老,她被迫背債1.2億!
Android9.0都來了,匯總一下安卓手機升級到Android8.0的現狀
LeCun、Jordan、李開復圓桌全文:AI的現狀與未來 | GMIC 2018
Counterpoint公布2018年第三季度中國手機市場現狀並展望雙十一
AppsFlyer 發布《2018年遊戲應用營銷現狀》報告
盤點韓流明星Super junior現狀,13人現在過得如何
Nature解析中國AI現狀,2030年能引領全球嗎?
受紅米Note 7Pro影響的魅族Note 9現狀如何?
2018年Q2法國線上市場手機品牌銷量現狀:蘋果隕落 僅iPhone6s上榜
ServerSAN的現狀分析
「EXO」「分享」181113 EXO成員宿舍分配現狀公開 吵鬧line和安靜line五五配比很科學
vivo創新不斷:驍龍8150 94%屏佔比 升降式雙攝 vivo不安於現狀
「假大空」的 Watson:IEEE Spectrum揭秘IBM混亂AI開發現狀
WebVR 現狀及2018年的趨勢
趙麗穎入住60000㎡壕宅 年末掃除怎麼做?(附血型×現狀 cleanup術)
Morketing曾巧:營銷產業的「3個改變」與「3大現狀」
Luwak Coffee:貓屎咖啡的現狀
2018CollegeFair北美名校嘉年華舉辦 教育專家分析高等教育現狀
數字視頻廣告現狀:門戶網站衰落,YouTube依舊稱霸,Facebook、Twitter奮起直追
Python最搶手、Go最有前途,7000位程序員揭秘2019軟體開發現狀