本篇文章給大家?guī)Т蠹伊私庖幌翹ode的兩種模塊規(guī)范(難以相容的 CJS 與 ESM),介紹一下CJS 和 ESM 的不同點(diǎn),怎么實(shí)現(xiàn) CJS、ESM 混寫,希望對大家有所幫助!
自 13.2.0
版本開始,Nodejs 在保留了 CommonJS(CJS)語法的前提下,新增了對 ES Modules(ESM)語法的支持。
天下苦 CJS 久已,Node 逐漸擁抱新標(biāo)準(zhǔn)的規(guī)劃當(dāng)然值得稱贊,我們也會展望未來 Node 不再需要借助工具,就能打破兩種模塊化語法的壁壘……
但實(shí)際上,一切沒有想象中的那么美好。
一、并不完美的 ESM 支持
1.1 在 Node 中使用 ESM
Node 默認(rèn)只支持 CJS 語法,這意味著你書寫了一個 ESM 語法的 js 文件,將無法被執(zhí)行。
如果想在 Node 中使用 ESM 語法,有兩種可行方式:
- ⑴ 在
package.json
中新增"type": "module"
配置項(xiàng)。 - ⑵ 將希望使用 ESM 的文件改為
.mjs
后綴。
對于第一種方式,Node 會將和 package.json
文件同路徑下的模塊,全部當(dāng)作 ESM 來解析。
第二種方式不需要修改 package.json
,Node 會自動地把全部 xxx.mjs
文件都作為 ESM 來解析。
同理,如果在
package.json
文件中設(shè)置"type": "commonjs"
,則表示該路徑下模塊以 CJS 形式來解析。 如果文件后綴名為.cjs
,Node 會自動地將其作為 CJS 模塊來解析(即使在package.json
中配置為 ESM 模式)。
我們可以通過上述修改 package.json
的方式,來讓全部模塊都以 ESM 形式執(zhí)行,然后項(xiàng)目上的模塊都統(tǒng)一使用 ESM 語法來書寫。
如果存在較多陳舊的 CJS 模塊懶得修改,也沒關(guān)系,把它們?nèi)颗驳揭粋€文件夾,在該文件夾路徑下新增一個內(nèi)容為 {"type": "commonjs"}
的 package.json
即可。
Node 在解析某個被引用的模塊時(無論它是被 import
還是被 require
),會根據(jù)被引用模塊的后綴名,或?qū)?yīng)的 package.json
配置去解析該模塊。
1.2 ESM 引用 CJS 模塊的問題
ESM 基本可以順利地 import
CJS 模塊,但對于具名的 exports(Named exports,即被整體賦值的 module.exports
),只能以 default export 的形式引入:
/** @file cjs/a.js **/ // named exports module.exports = { foo: () => { console.log("It's a foo function...") } } /** @file index_err.js **/ import { foo } from './cjs/a.js'; // SyntaxError: Named export 'foo' not found. The requested module './cjs/a.js' is a CommonJS module, which may not support all module.exports as named exports. foo(); /** @file index_err.js **/ import pkg from './cjs/a.js'; // 以 default export 的形式引入 pkg.foo(); // 正常執(zhí)行
到 Github 獲取示例代碼(test1):
https://github.com/VaJoy/BlogDemo3/tree/main/220220/test1
具體原因我們會在后續(xù)提及。
1.3 CJS 引用 ESM 模塊的問題
假設(shè)你在開發(fā)一個供別人使用的開源項(xiàng)目,且使用 ESM 的形式導(dǎo)出模塊,那么問題來了 —— 目前 CJS 的 require
函數(shù)無法直接引入 ESM 包,會報錯:
let { foo } = require('./esm/b.js'); ^ Error [ERR_REQUIRE_ESM]: require() of ES Module BlogDemo3220220test2esmb.js from BlogDemo3220220test2require.js not supported. Instead change the require of b.js in BlogDemo3220220test2require.js to a dynamic import() which is available in all CommonJS modules. at Object.<anonymous> (BlogDemo3220220test2require.js:4:15) { code: 'ERR_REQUIRE_ESM' }
按照上述錯誤陳述,我們不能并使用 require
引入 ES 模塊(原因會在后續(xù)提及),應(yīng)當(dāng)改為使用 CJS 模塊內(nèi)置的動態(tài) import
方法:
import('./esm/b.js').then(({ foo }) => { foo(); }); // or (async () => { const { foo } = await import('./esm/b.js'); })();
到 Github 獲取示例代碼(test2):
https://github.com/VaJoy/BlogDemo3/tree/main/220220/test2
查閱 dynamic import 文檔
https://v8.dev/features/dynamic-import#dynamic
開源項(xiàng)目當(dāng)然不能強(qiáng)制要求用戶改用這種形式來引入,所以又得借助 rollup 之類的工具將項(xiàng)目編譯為 CJS 模塊……
由上可見目前 Node.js 對 ESM 語法的支持是有限制的,如果不借助工具處理,這些限制可能會很糟心。
對于想入門前端的新手來說,這些麻煩的規(guī)則和限制也會讓人困惑。
截至我落筆書寫本文時, Node.js LTS 版本為 16.14.0
,距離開始支持 ESM 的 13.2.0
版本已過去了兩年多的時間。
那么為何 Node.js 到現(xiàn)在還無法打通 CJS 和 ESM?
答案并非 Node.js 敵視 ESM 標(biāo)準(zhǔn)從而遲遲不做優(yōu)化,而是因?yàn)?—— CJS 和 ESM,二者真是太不一樣了。
二、CJS 和 ESM 的不同點(diǎn)
2.1 不同的加載邏輯
在 CJS 模塊中,require()
是一個同步接口,它會直接從磁盤(或網(wǎng)絡(luò))讀取依賴模塊并立即執(zhí)行對應(yīng)的腳本。
ESM 標(biāo)準(zhǔn)的模塊加載器則完全不同,它讀取到腳本后不會直接執(zhí)行,而是會先進(jìn)入編譯階段進(jìn)行模塊解析,檢查模塊上調(diào)用了 import
和 export
的地方,并順騰摸瓜把依賴模塊一個個異步、并行地下載下來。
在此階段 ESM 加載器不會執(zhí)行任何依賴模塊代碼,只會進(jìn)行語法檢錯、確定模塊的依賴關(guān)系、確定模塊輸入和輸出的變量。
最后 ESM 會進(jìn)入執(zhí)行階段,按順序執(zhí)行各模塊腳本。
所以我們常常會說,CommonJS 模塊是運(yùn)行時加載,ES6 模塊是編譯時輸出接口。
在上方 1.2 小節(jié),我們曾提及到 ESM 中無法通過指定依賴模塊屬性的形式引入 CJS named exports:
/** @file cjs/a.js **/ // named exports module.exports = { foo: () => { console.log("It's a foo function...") } } /** @file index_err.js **/ import { foo } from './cjs/a.js'; // SyntaxError: Named export 'foo' not found. The requested module './cjs/a.js' is a CommonJS module, which may not support all module.exports as named exports. foo();
這是因?yàn)?ESM 獲取所指定的依賴模塊屬性(花括號內(nèi)部的屬性),是需要在編譯階段進(jìn)行靜態(tài)分析的,而 CJS 的腳本要在執(zhí)行階段才能計算出它們的 named exports 的值,會導(dǎo)致 ESM 在編譯階段無法進(jìn)行分析。
2.2 不同的模式
ESM 默認(rèn)使用了嚴(yán)格模式(use strict
),因此在 ES 模塊中的 this
不再指向全局對象(而是 undefined
),且變量在聲明前無法使用。
這也是為何在瀏覽器中,<script>
標(biāo)簽如要啟用原生引入 ES 模塊能力,必須加上 type="module"
告知瀏覽器應(yīng)當(dāng)把它和常規(guī) JS 區(qū)分開來處理。
查看 ESM 嚴(yán)格模式的