CJS 、 ESM 與 Tree Shaking
目錄
- 目錄
- ESM 和 CJS 是完全不同的
- ESM 改變了很多事情
- ESM 靜態模組結構 (static module structure)
- ESM 不能 import 命名導出的 CJS
- CJS 不能 require() ESM
- CJS 可以 import() ESM 但不是很方便
- 如何使 “CJS” 的
default exports
可以使用named export
- 結語
- 參考
從 Node 誕生開始 CommonJS(CJS)就被廣泛使用來進行開發,透過 require()
引入它們,當實作了 script,提供給他人使用,可以定義 exports
,使用 named exports module.exports.foo = 'bar'
或是 default exports module.exports = 'baz'
的方式導出 script。
ESM 和 CJS 是完全不同的。儘管從表面上來看,ESM 看起來非常相似。
使用 命名導出 的 CJS 範例
1 | // @filename: utils.CJS |
使用 default exports 的 CJS 範例
1 | // @filenameL util.CJS |
在 ESM 中,使用 import
與 export
,對於 default export 和 named export 也有兩種不同的語法。
使用 named export 的 ESM 範例。
1 | // @filename: util.mjs |
使用 default export 的 ESM 範例。
1 | // @filename: util.mjs |
ESM 和 CJS 是完全不同的
- 在 CJS 中,
imports
會 複製 一份 exports 的內容來使用。 - 在 ESM 中,
imports
是使用 靜態結構 的read-only view
來使用裡面的內容。
CJS
是 require
是同步的,它不會回傳 Promise
或 callback
,
ESM
以非同步的方式加載,它依賴於 ES6 aka ES2015
的 靜態結構。
之後 Module Loader
會下載以及解析我們導入的所有 scripts,再來會輪到 scripts 所依賴的 scripts,然後建構依賴的 模組地圖
(ES module graph),直到找到沒有再導入任何 scripts 的 script。最後,允許執行該 scripts,再允許運行該 script 依賴的 script,以此類推。
建構出來的 模組地圖
(ES module graph) 的 scripts 都是併行下載的,但它們會按照順序執行,由 Module Loader
的規範保證。
ESM 改變了很多事情
ESM 改變了 Javascript 很多事情,首先,ESM script 默認使用嚴格模式 (use strict),它們的 this
不會引用全局物件,作用域的運作方式不同等等。
這就是為什麼即使在瀏覽器中,<script> 標籤的默認設定不是 ESM 的;必須添加 type="module"
才可以進入 ESM 模式,將默認設定從 CJS 切換到 ESM 將會是對兼容性的重大突破。
ESM 靜態模組結構 (static module structure)
靜態結構的意思是可以在編譯期間 靜態的 確定 import
或 export
,ESM Loader
會在不運行程式碼的情況下解析 import
與 export
;在解析階段, ESM Loader
可以在沒有真正 運行程式碼之前 立即的找出 命名導入
因拼寫錯誤而引發的異常。
只能在程式碼最上層 (top-level) 使用 export 或 import
(不能放在條件式中),並且 import
和 export
(不允許使用變數)。
下面是沒有 靜態結構 的 CJS 的兩個範例:
1 | var my_lib; |
如果沒有給 else
,您必須運行代碼以找出它導出的內容:
1 | if (Math.random()) { |
ESM 會強制保持靜態結構,因此可以得到好處:
Tree Shaking
可以將應用程式想像成一棵樹,實際使用到的 code 和 library 代表了綠色的還活著的葉子。死掉的程式碼代表枯萎的褐色葉子,為了擺脫枯葉,必須搖動樹,使它們掉落。
在開發過程中,模組通常會這樣處理:
- 開發過程,程式碼存在在許多模組 (通常很小)
- 執行部屬,這些模組會被 bundle 並分類到幾個較大的區塊中
bundle 的原因:
- 下載 bundle 時需要索引的文件更少
- 壓縮 bundle 後的檔案擁有更好的效能
- 可以在 bundle 期間刪除不必要的檔案
為了利用 Tree Shaking
的優勢有幾點需要注意:
- 必須使用 ESM (
import
和export
) - 確保編譯器沒有將 ESM 轉換成 CJS (這是 Babel 預設 @Babel 預設 @babel/preset-env 的默認行為,參閱 文檔 )
- 將
sideEffects
屬性加入倒 package.json - 啟用
webpack
的production mode
配置開啟 各種優化。
更緊湊的 bundle
The module bundler Rollup 證明了 ESM 可以有效的組合,因為它們都適合單個範圍 (在重命名變數以消除變數衝突之後),這也仰賴於 ESM 的兩個特性:
都是靜態結構意味著 bundle 格式不需要考慮條件式載入的模組
import
是export
的 read-only view。這意味著不必複製一份並導出,可以直接使用它們。
例如,以下兩個 ESM 的範例
1 | // lib.js |
Rollup 會將這兩個模組綁到單個 ES6 模組之下(刪除沒用到的 bar)
1 | function foo() {} |
更快的查詢並導入
如果需要 import CJS 的 libary,會得到一個物件:
1 | var lib = require(lib); |
必須通過屬性去使用 lib.someFunc
,這樣沒有效率,因為它是動態結構。
相反的,如果是使用 ESM import 某個 library,因為是靜態結構,我們可以很清楚的知道它裡面有什麼變數可以使用。
1 | import * as lib from "lib"; |
ESM 不能 import 命名導出的 CJS
可以這樣做
1 | import _ from "lodash.CJS "; |
但不能這樣
1 | import { shuffle } from "lodash.CJS"; |
這是因為 CJS script 是在執行期間計算 命名導出
,而 ESM 則是在解析階段計算。
ESM 可以使用 require
require
在 ESM script 默認不在使用範圍內,但可以透過以下方式使用
1 | import { createRequire } from "module"; |
但是這樣的方式沒有實質上的幫助,實際上只是需要撰寫更多的程式碼,而且不僅僅是執行 import
與解構,像 webpack、Rollup 這樣的打包工具也不知道該怎麼處理 createRequire
模式,這樣就變得沒那麼有意義。
CJS 不能 require() ESM
CJS 不能 require
ESM 的最簡單原因是因為 ESM 可以做 top-level await
,而 CJS scripts 不行。
top-level
await
可以讓我們在 async function 之外使用await
關鍵字。
ESM 的 multi-pasge loader 使 ESM 可以實現 top-levelawait
。
由於 CJS 不支援 top-level await
,因此甚至不可能將 ESM top-level await
轉換為 CJS。
CJS 可以 import() ESM 但不是很方便
如果想要使用 CJS import ESM script,可以使用 dynamic import 的方式:
1 | async () => { |
如何使 “CJS” 的 default exports
可以使用 named export
為 CJS 命名導出提供一個 ESM wrapper
為 CJS libraries 提供 ESM wrapper 很容易,但永遠不可能為 ESM libraries 提供 CJS wrapper
1 | import CJSModule from "../index.js"; |
將 ESM wrapper 放在 ESM
的子目錄,同層的 package.json
的 增加 {"type":"module}
.
結語
- CJS 使用
require
、exports
,ESM 使用import
、export
- ESM 和 CJS 是完全不同的
- CJS 是同步的,它不會回傳
promise
或callback
且是動態結構,ESM Module Loader 是異步的,並在運行前解析成靜態結構 - ESM 解析出來的
Module Graph
會並行下載但會依序執行。 - ESM 強制的靜態結構帶來了許多好處。
- ESM 不能 import
命名導出
的 CJS。