教你徹底搞懂ESM與CJS互相轉(zhuǎn)換
正文
ESM 和 CJS 是我們常用的模塊格式,兩種模塊系統(tǒng)具有不同的語(yǔ)法和加載機(jī)制。在項(xiàng)目中,我們可能會(huì)遇到 ESM 和 CJS 轉(zhuǎn)換的場(chǎng)景:
- ESM 引入只支持 CJS 的庫(kù)
- 開(kāi)發(fā) npm 庫(kù)的時(shí)候,寫(xiě) ESM 然后編譯成 CJS。
- ……
最近在項(xiàng)目中也剛好遇到的轉(zhuǎn)換上的一些問(wèn)題,于是就研究了一下
本文將介紹 ESM 和 CJS 之間轉(zhuǎn)換,幫助大家加深對(duì)它們的了解,并從中了解它們之間轉(zhuǎn)換的細(xì)節(jié)與局限性
ESM 轉(zhuǎn) CJS
ESM 轉(zhuǎn) CJS 的使用場(chǎng)景非常常見(jiàn),例如:
- npm 庫(kù),需要同時(shí)提供 ESM 和 CJS,供開(kāi)發(fā)者自行選擇使用。一般是用 ESM 開(kāi)發(fā),然后同時(shí)輸出 ESM 和 CJS
- 使用 ESM 進(jìn)行開(kāi)發(fā),但最后由于兼容性、性能等原因,編譯成 CJS 在線上運(yùn)行。例如:利用 Vite、webpack 等構(gòu)建工具進(jìn)行開(kāi)發(fā) 開(kāi)發(fā)
各大工具,如 TSC、Babel、Vite、webpack、Rollup 等,都自帶了 ESM 轉(zhuǎn) CJS 的能力。
export 的轉(zhuǎn)換
- 情況一,只有默認(rèn)導(dǎo)出:
export default 666
Rollup 會(huì)轉(zhuǎn)換成
modules.exports = 666
很好理解,modules.exports
導(dǎo)出的整個(gè)東西就是默認(rèn)導(dǎo)出嘛
用 CJS 引用該模塊的方式:
const lib = require('lib') console.log(lib) // 666
- 情況二,只有命名導(dǎo)出:
export const a = 123 export const b = 234
轉(zhuǎn)換成
module.exports.a = 123 module.exports.b = 234
命名導(dǎo)出用 module.exports.xxx
一個(gè)個(gè)導(dǎo)出就行
用 CJS 引用該模塊的方式:
const {a, b} = require('lib') console.log(a, b) // 123 234
- 情況三:默認(rèn)導(dǎo)出和命名導(dǎo)出同時(shí)存在
export default 666 export const a = 123 export const b = 234
這時(shí)候會(huì)發(fā)現(xiàn),前面兩種情況的轉(zhuǎn)換思路不能用了,你不能這樣轉(zhuǎn)換
modules.exports = 666 module.exports.a = 123 module.exports.b = 234
畢竟 modules.exports
不是對(duì)象,因此設(shè)置不了屬性。
那莫得辦法了,只能這樣表示了:
module.exports.default
為默認(rèn)導(dǎo)出module.exports.xxx
其他為命名導(dǎo)出
為了跟前兩種情況做區(qū)分,因此還要新增一個(gè)標(biāo)記__esModule
于是就會(huì)編譯成這樣的代碼:
+ Object.defineProperty(exports, '__esModule', { value: true }) + module.exports.default = 666 - module.exports = 666 module.exports.a = 123 module.exports.b = 234
用 CJS 引用該模塊的方式:
const lib = require('lib') console.log(lib.default, lib.a, lib.b) // 666 123 234
在這種情況下,必須要用 .default
訪問(wèn)默認(rèn)導(dǎo)出
但這樣子看起來(lái)非常的別扭,但是沒(méi)有辦法,混用默認(rèn)導(dǎo)出和命名導(dǎo)出是有代價(jià)的。
為什么我們項(xiàng)目中,從來(lái)就遇到過(guò)該問(wèn)題?
一般情況下,我們使用 ESM 寫(xiě)項(xiàng)目,然后編譯成 CJS
假如,我們寫(xiě)的代碼引用了上述的代碼(默認(rèn)導(dǎo)出和命名導(dǎo)出混用):
// foo.js import lib from 'lib' import {a, b} from 'lib' console.log(lib, a, b)
這段代碼,會(huì)被轉(zhuǎn)換成:
'use strict'; var lib = require('lib'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var lib__default = /*#__PURE__*/_interopDefault(lib); console.log(lib__default.default, lib.a, lib.b);
_interopDefault
函數(shù)會(huì)自動(dòng)根據(jù) __esModule
,將導(dǎo)出對(duì)象標(biāo)準(zhǔn)化,使 .default
一定為默認(rèn)導(dǎo)出
- 如果有
__esModule
,那就不用處理 - 沒(méi)有
__esModule
,就將其放到default
屬性中,作為默認(rèn)導(dǎo)出
工具在轉(zhuǎn)譯 lib.js
的同時(shí),也會(huì)轉(zhuǎn)譯引入它的 foo.js
,會(huì)加上標(biāo)準(zhǔn)化 require
對(duì)象的邏輯。
我們的項(xiàng)目,在編譯的時(shí)候,全部 ESM 模塊都轉(zhuǎn)為 CJS(不是只轉(zhuǎn)換一個(gè),不轉(zhuǎn)另外一個(gè)) ,在這個(gè)過(guò)程中它自動(dòng)屏蔽了模塊默認(rèn)導(dǎo)出的差異,由于編譯工具已經(jīng)幫我們處理好,因此我們沒(méi)有任何感知。
如果我們直接寫(xiě) CJS,去引入 ESM 轉(zhuǎn)換后的 CJS,就需要自行處理該問(wèn)題
要想盡量避免這種情況,建議全部都使用命名導(dǎo)出,由于沒(méi)有默認(rèn)導(dǎo)出,就不需要擔(dān)心默認(rèn)導(dǎo)出是 module.exports
還是 module.exports.default
,都用以下方式進(jìn)行引入即可:
const {a, b} = require('lib')
這樣開(kāi)發(fā)者在任何情況下都沒(méi)有心智負(fù)擔(dān)。
import 的轉(zhuǎn)換
其實(shí)上一小節(jié)已經(jīng)講了
import lib from 'lib' import {a, b} from 'lib' console.log(lib, a, b)
會(huì)被轉(zhuǎn)換成
'use strict'; var lib = require('lib'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var lib__default = /*#__PURE__*/_interopDefault(lib); console.log(lib__default.default, lib.a, lib.b);
加上 _interopDefault
,屏蔽了不同情況下默認(rèn)導(dǎo)出的差異,因此如果所有代碼都是從 ESM 轉(zhuǎn) CJS,就不用擔(dān)心默認(rèn)導(dǎo)出的差異問(wèn)題。
小結(jié)
其實(shí) ESM 轉(zhuǎn) CJS,不同的工具的輸出會(huì)稍微有些不同。以上是 Rollup 的的轉(zhuǎn)換方式,個(gè)人認(rèn)為這種更為簡(jiǎn)潔,而 TSC 的轉(zhuǎn)換則更復(fù)雜。
不過(guò)這些工具的思路都是相同的,都遵守 __esModule
的約定,標(biāo)記 __esModule
的模塊默認(rèn)導(dǎo)出是 .default
ESM 轉(zhuǎn) CJS 有哪些局限性?
存在以下情況可能無(wú)法進(jìn)行轉(zhuǎn)換:
- 存在循環(huán)依賴(lài)
- import.meta,這個(gè)特性只能在 ESM 中使用
CJS 轉(zhuǎn) ESM
CJS 轉(zhuǎn) ESM 的場(chǎng)景不多,一般不會(huì)用 CJS 寫(xiě) npm 庫(kù)然后輸出 ESM;用 CJS 寫(xiě)的庫(kù),當(dāng)時(shí)不會(huì)輸出 ESM。新寫(xiě)的 npm 庫(kù),一般來(lái)說(shuō)也是用 ESM 寫(xiě)。
因此一般只有寫(xiě) ESM 項(xiàng)目,引入了一個(gè)只有 CJS 的庫(kù)時(shí),且編譯出 ESM 時(shí),才會(huì)用到 CJS 轉(zhuǎn) ESM。
為什么我們用 webpack 寫(xiě) ESM,然后引入 CJS 的時(shí)候,基本上沒(méi)遇到什么問(wèn)題?
要運(yùn)行 ESM 引入 CJS 的代碼,有兩種方式:
- 把 ESM 轉(zhuǎn) CJS,然后運(yùn)行 CJS
- 把 CJS 轉(zhuǎn)成 ESM,然后運(yùn)行 ESM
因?yàn)?webpack 是前者,ESM 轉(zhuǎn) CJS 能夠很好地進(jìn)行轉(zhuǎn)換。
CJS 轉(zhuǎn) ESM,沒(méi)有一種統(tǒng)一的轉(zhuǎn)換標(biāo)準(zhǔn)(相對(duì)來(lái)說(shuō),ESM 轉(zhuǎn) CJS 有 __esModule
約定),不同的工具和庫(kù),可能轉(zhuǎn)換出來(lái)的結(jié)果是不一樣的,可能會(huì)導(dǎo)致代碼不兼容。
export 的轉(zhuǎn)換
場(chǎng)景一:
module.exports = { a: 3, b: 4 }
Rollup 會(huì)轉(zhuǎn)換成
var lib = { a: 3, b: 4 }; export { lib as default };
module.exports
會(huì)被當(dāng)做默認(rèn)導(dǎo)出
而 esbuild 會(huì)這樣轉(zhuǎn)換
var __getOwnPropNames = Object.getOwnPropertyNames; var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var require_lib = __commonJS({ "src/cjs/lib.js"(exports, module) { module.exports = { a: 3, b: 4 }; } }); export default require_lib();
esbuild 會(huì)給代碼包一層輔助函數(shù),然后將代碼搬過(guò)去就好了。好處是,這樣編譯工具就不需要考慮代碼的真正意義,直接簡(jiǎn)單包一層即可
這種情況下,雖然 Rollup 和 esbuild 轉(zhuǎn)換的代碼不太相同,但代碼的運(yùn)行結(jié)果是相同的
場(chǎng)景二:
exports.c =123
Rollup 會(huì)轉(zhuǎn)換成:
var lib = {}; var c = lib.c =123; export { c, lib as default };
Rollup 會(huì)轉(zhuǎn)換成默認(rèn)導(dǎo)出和命名導(dǎo)出。
esbuild 則轉(zhuǎn)換成:
var __getOwnPropNames = Object.getOwnPropertyNames; var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var require_lib = __commonJS({ "src/cjs/lib.js"(exports) { exports.d = 666; } }); export default require_lib();
仍然是包一層輔助函數(shù),但 esbuild 全部都當(dāng)做默認(rèn)導(dǎo)出
在這種情況下,Rollup 和 esbuild 轉(zhuǎn)換的代碼,其運(yùn)行結(jié)果是不同的
場(chǎng)景三:
exports.d = 123 module.exports = { a: 3, b: 4 } exports.c =123
exports.d = 123
其實(shí)是無(wú)效的
Rollup 會(huì)編譯成這樣:
var libExports = {}; var lib$1 = { get exports(){ return libExports; }, set exports(v){ libExports = v; }, }; (function (module, exports) { exports.d =123; module.exports = { a: 3, b: 4 }; exports.c =123; } (lib$1, libExports)); var lib = libExports; export { lib as default };
此時(shí) Rollup 也會(huì)加上一層輔助函數(shù)
而 esbuild 仍然是加一層輔助函數(shù)
var __getOwnPropNames = Object.getOwnPropertyNames; var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var require_lib = __commonJS({ "src/cjs/lib.js"(exports, module) { exports.d = 666; module.exports = { a: 3, b: 4 }; exports.c = 666; } }); export default require_lib();
輔助函數(shù)的好處之前也說(shuō)了,不需要關(guān)注代碼邏輯,可以看到,即使 exports.d = 666;
是一行無(wú)效語(yǔ)句,照樣執(zhí)行也是沒(méi)有問(wèn)題的,不需要先分析出代碼的語(yǔ)義。
總體對(duì)比下來(lái),esbuild
的處理還是相對(duì)簡(jiǎn)單的
require 的轉(zhuǎn)換
const lib = require('./lib') const {c} = require('./lib') console.info(lib,c)
Rollup 轉(zhuǎn)換成:
import require$$0 from './lib'; const lib = require$$0; const {c} = require$$0; console.info(lib,c);
require 的轉(zhuǎn)換比較簡(jiǎn)單,不管你解不解構(gòu),反正我就只有默認(rèn)引入
而 esbuild。。。還不支持,干脆就報(bào)錯(cuò)了
小結(jié)
為什么工具的轉(zhuǎn)換結(jié)果是不同的?
CJS 轉(zhuǎn)換成 ESM 是有歧義的
module.export.a = 123 module.export.b = 345
等價(jià)于
module.export = { a: 123, b: 345, }
那么它是默認(rèn)導(dǎo)出,還是命名導(dǎo)出呢?都行
本質(zhì)上,是因?yàn)?CJS 只有一個(gè)導(dǎo)出方式,不確定它對(duì)應(yīng)的是 ESM 的命名導(dǎo)出還是默認(rèn)導(dǎo)出。
用一個(gè)形象點(diǎn)的例子就是,女朋友回了一句哦,但是你不知道女朋友是想說(shuō)肯定的意思,還是表示無(wú)語(yǔ)的意思、還是其他別的意思。。。
對(duì)于 require
const {c} = require('./lib')
你說(shuō)這個(gè)是默認(rèn)導(dǎo)入呢?還是命名導(dǎo)入?好像也都行。。。
正是由于這個(gè)歧義,且沒(méi)有一個(gè)標(biāo)準(zhǔn)去規(guī)范這個(gè)轉(zhuǎn)換行為,因此不同工具的轉(zhuǎn)換結(jié)果是不同的
CJS 轉(zhuǎn)換成 ESM 有哪些局限性?
- 不同工具的轉(zhuǎn)換結(jié)果不同
- CJS 模塊可以使用
require.resolve
方法查找模塊的路徑,而 ESM 模塊不可以 - CJS 模塊可以導(dǎo)入和導(dǎo)出非 JavaScript 文件,例如 JSON
- CJS 在運(yùn)行時(shí)導(dǎo)入導(dǎo)出,支持運(yùn)行時(shí)改變導(dǎo)入導(dǎo)出的內(nèi)容,以下代碼是合法的:
module.exports.a = 123 if( Date.now() % 2){ module.exports.b = 234 }
由于沒(méi)有統(tǒng)一的標(biāo)準(zhǔn),CJS 轉(zhuǎn) ESM 的工具,相對(duì)來(lái)說(shuō)少了很多,目前僅有少量工具能夠進(jìn)行轉(zhuǎn)換,esbuild
、babel-plugin-transform-commonjs
、@rollup/commonjs
。
有時(shí)候 Vite 使用一些 CJS 包不兼容,也是因?yàn)橛行?CJS 轉(zhuǎn)不了 ESM。但幸運(yùn)的是,目前大部分常見(jiàn)的 npm 包,都已經(jīng)支持 ESM,或者能夠比較好的被轉(zhuǎn)換成 ESM,因此也不需要太擔(dān)心 Vite 的問(wèn)題。
以上就是教你徹底搞懂ESM與CJS互相轉(zhuǎn)換的詳細(xì)內(nèi)容,更多關(guān)于ESM與CJS互相轉(zhuǎn)換的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Node.JS中事件輪詢(xún)(Event Loop)的解析
對(duì)NodeJs的事情輪詢(xún)機(jī)造一孔之見(jiàn)。查閱了些許材料后,總算掀開(kāi)了其神奇的里紗。下面這篇文章主要介紹了Node.JS中事件輪詢(xún)(Event Loop)的相關(guān)資料,需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-02-02Mac下通過(guò)brew安裝指定版本的nodejs教程
今天小編就為大家分享一篇Mac下通過(guò)brew安裝指定版本的nodejs教程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-05-05nodeJs實(shí)現(xiàn)基于連接池連接mysql的方法示例
這篇文章主要介紹了nodeJs實(shí)現(xiàn)基于連接池連接mysql的方法,結(jié)合具體實(shí)例形式分析了nodejs連接池操作mysql數(shù)據(jù)庫(kù)連接的實(shí)現(xiàn)與使用技巧,需要的朋友可以參考下2018-02-02Node.js+Express+Mysql 實(shí)現(xiàn)增刪改查
這篇文章主要介紹了Node.js+Express+Mysql 實(shí)現(xiàn)增刪改查,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04利用nodejs讀取圖片并將二進(jìn)制數(shù)據(jù)轉(zhuǎn)換成base64格式
這篇文章主要介紹了利用nodejs讀取圖片并將二進(jìn)制數(shù)據(jù)轉(zhuǎn)換成base64格式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08node.js使用stream模塊實(shí)現(xiàn)自定義流示例
這篇文章主要介紹了node.js使用stream模塊實(shí)現(xiàn)自定義流,結(jié)合實(shí)例形式詳細(xì)分析了node.js基于stream模塊實(shí)現(xiàn)自定義的可讀流、可寫(xiě)流、可讀寫(xiě)流等相關(guān)操作技巧,需要的朋友可以參考下2020-02-02利用Node.js和MySQL實(shí)現(xiàn)創(chuàng)建API服務(wù)器
這篇文章主要為大家詳細(xì)介紹了如何使用Node.js和MySQL創(chuàng)建API服務(wù)器的步驟,這也是從前端邁向全棧的一個(gè)開(kāi)始,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解下2024-01-01Node.js全局可用變量、函數(shù)和對(duì)象示例詳解
JavaScript中有一個(gè)特殊的對(duì)象,稱(chēng)為全局對(duì)象(Global Object),它及其所有屬性都可以在程序的任何地方訪問(wèn),即全局變量,下面這篇文章主要給大家介紹了關(guān)于Node.js全局可用變量、函數(shù)和對(duì)象的相關(guān)資料,需要的朋友可以參考下2023-03-03