您好,登錄后才能下訂單哦!
本篇內容介紹了“ESM與CJS互相轉換怎么實現”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
ESM 和 CJS 是我們常用的模塊格式,兩種模塊系統具有不同的語法和加載機制。在項目中,我們可能會遇到 ESM 和 CJS 轉換的場景:
ESM 引入只支持 CJS 的庫
開發 npm 庫的時候,寫 ESM 然后編譯成 CJS。
ESM 轉 CJS 的使用場景非常常見,例如:
npm 庫,需要同時提供 ESM 和 CJS,供開發者自行選擇使用。一般是用 ESM 開發,然后同時輸出 ESM 和 CJS
使用 ESM 進行開發,但最后由于兼容性、性能等原因,編譯成 CJS 在線上運行。例如:利用 Vite、webpack 等構建工具進行開發 開發
各大工具,如 TSC、Babel、Vite、webpack、Rollup 等,都自帶了 ESM 轉 CJS 的能力。
情況一,只有默認導出:
export default 666
Rollup 會轉換成
modules.exports = 666
很好理解,modules.exports
導出的整個東西就是默認導出嘛
用 CJS 引用該模塊的方式:
const lib = require('lib') console.log(lib) // 666
情況二,只有命名導出:
export const a = 123 export const b = 234
轉換成
module.exports.a = 123 module.exports.b = 234
命名導出用 module.exports.xxx
一個個導出就行
用 CJS 引用該模塊的方式:
const {a, b} = require('lib') console.log(a, b) // 123 234
情況三:默認導出和命名導出同時存在
export default 666 export const a = 123 export const b = 234
這時候會發現,前面兩種情況的轉換思路不能用了,你不能這樣轉換
modules.exports = 666 module.exports.a = 123 module.exports.b = 234
畢竟 modules.exports
不是對象,因此設置不了屬性。
那莫得辦法了,只能這樣表示了:
module.exports.default
為默認導出
module.exports.xxx
其他為命名導出
為了跟前兩種情況做區分,因此還要新增一個標記__esModule
于是就會編譯成這樣的代碼:
+ 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
訪問默認導出
但這樣子看起來非常的別扭,但是沒有辦法,混用默認導出和命名導出是有代價的。
為什么我們項目中,從來就遇到過該問題?
一般情況下,我們使用 ESM 寫項目,然后編譯成 CJS
假如,我們寫的代碼引用了上述的代碼(默認導出和命名導出混用):
// foo.js import lib from 'lib' import {a, b} from 'lib' console.log(lib, a, b)
這段代碼,會被轉換成:
'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
函數會自動根據 __esModule
,將導出對象標準化,使 .default
一定為默認導出
如果有 __esModule
,那就不用處理
沒有 __esModule
,就將其放到 default
屬性中,作為默認導出
工具在轉譯 lib.js
的同時,也會轉譯引入它的 foo.js
,會加上標準化 require
對象的邏輯。
我們的項目,在編譯的時候,全部 ESM 模塊都轉為 CJS(不是只轉換一個,不轉另外一個) ,在這個過程中它自動屏蔽了模塊默認導出的差異,由于編譯工具已經幫我們處理好,因此我們沒有任何感知。
如果我們直接寫 CJS,去引入 ESM 轉換后的 CJS,就需要自行處理該問題
要想盡量避免這種情況,建議全部都使用命名導出,由于沒有默認導出,就不需要擔心默認導出是 module.exports
還是 module.exports.default
,都用以下方式進行引入即可:
const {a, b} = require('lib')
這樣開發者在任何情況下都沒有心智負擔。
其實上一小節已經講了
import lib from 'lib' import {a, b} from 'lib' console.log(lib, a, b)
會被轉換成
'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
,屏蔽了不同情況下默認導出的差異,因此如果所有代碼都是從 ESM 轉 CJS,就不用擔心默認導出的差異問題。
其實 ESM 轉 CJS,不同的工具的輸出會稍微有些不同。以上是 Rollup 的的轉換方式,個人認為這種更為簡潔,而 TSC 的轉換則更復雜。
不過這些工具的思路都是相同的,都遵守 __esModule
的約定,標記 __esModule
的模塊默認導出是 .default
ESM 轉 CJS 有哪些局限性?
存在以下情況可能無法進行轉換:
存在循環依賴
import.meta,這個特性只能在 ESM 中使用
CJS 轉 ESM 的場景不多,一般不會用 CJS 寫 npm 庫然后輸出 ESM;用 CJS 寫的庫,當時不會輸出 ESM。新寫的 npm 庫,一般來說也是用 ESM 寫。
因此一般只有寫 ESM 項目,引入了一個只有 CJS 的庫時,且編譯出 ESM 時,才會用到 CJS 轉 ESM。
為什么我們用 webpack 寫 ESM,然后引入 CJS 的時候,基本上沒遇到什么問題?
要運行 ESM 引入 CJS 的代碼,有兩種方式:
把 ESM 轉 CJS,然后運行 CJS
把 CJS 轉成 ESM,然后運行 ESM
因為 webpack 是前者,ESM 轉 CJS 能夠很好地進行轉換。
CJS 轉 ESM,沒有一種統一的轉換標準(相對來說,ESM 轉 CJS 有 __esModule
約定),不同的工具和庫,可能轉換出來的結果是不一樣的,可能會導致代碼不兼容。
場景一:
module.exports = { a: 3, b: 4 }
Rollup 會轉換成
var lib = { a: 3, b: 4 }; export { lib as default };
module.exports
會被當做默認導出
而 esbuild 會這樣轉換
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 會給代碼包一層輔助函數,然后將代碼搬過去就好了。好處是,這樣編譯工具就不需要考慮代碼的真正意義,直接簡單包一層即可
這種情況下,雖然 Rollup 和 esbuild 轉換的代碼不太相同,但代碼的運行結果是相同的
場景二:
exports.c =123
Rollup 會轉換成:
var lib = {}; var c = lib.c =123; export { c, lib as default };
Rollup 會轉換成默認導出和命名導出。
esbuild 則轉換成:
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();
仍然是包一層輔助函數,但 esbuild 全部都當做默認導出
在這種情況下,Rollup 和 esbuild 轉換的代碼,其運行結果是不同的
場景三:
exports.d = 123 module.exports = { a: 3, b: 4 } exports.c =123
exports.d = 123
其實是無效的
Rollup 會編譯成這樣:
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 };
此時 Rollup 也會加上一層輔助函數
而 esbuild 仍然是加一層輔助函數
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();
輔助函數的好處之前也說了,不需要關注代碼邏輯,可以看到,即使 exports.d = 666;
是一行無效語句,照樣執行也是沒有問題的,不需要先分析出代碼的語義。
總體對比下來,esbuild
的處理還是相對簡單的
const lib = require('./lib') const {c} = require('./lib') console.info(lib,c)
Rollup 轉換成:
import require$$0 from './lib'; const lib = require$$0; const {c} = require$$0; console.info(lib,c);
require 的轉換比較簡單,不管你解不解構,反正我就只有默認引入
而 esbuild。。。還不支持,干脆就報錯了
為什么工具的轉換結果是不同的?
CJS 轉換成 ESM 是有歧義的
module.export.a = 123 module.export.b = 345
等價于
module.export = { a: 123, b: 345, }
那么它是默認導出,還是命名導出呢?都行
本質上,是因為 CJS 只有一個導出方式,不確定它對應的是 ESM 的命名導出還是默認導出。
用一個形象點的例子就是,女朋友回了一句哦,但是你不知道女朋友是想說肯定的意思,還是表示無語的意思、還是其他別的意思。。。
對于 require
const {c} = require('./lib')
你說這個是默認導入呢?還是命名導入?好像也都行。。。
正是由于這個歧義,且沒有一個標準去規范這個轉換行為,因此不同工具的轉換結果是不同的
CJS 轉換成 ESM 有哪些局限性?
不同工具的轉換結果不同
CJS 模塊可以使用 require.resolve
方法查找模塊的路徑,而 ESM 模塊不可以
CJS 模塊可以導入和導出非 JavaScript 文件,例如 JSON
CJS 在運行時導入導出,支持運行時改變導入導出的內容,以下代碼是合法的:
module.exports.a = 123 if( Date.now() % 2){ module.exports.b = 234 }
由于沒有統一的標準,CJS 轉 ESM 的工具,相對來說少了很多,目前僅有少量工具能夠進行轉換,esbuild
、babel-plugin-transform-commonjs
、@rollup/commonjs
。
有時候 Vite 使用一些 CJS 包不兼容,也是因為有些 CJS 轉不了 ESM。但幸運的是,目前大部分常見的 npm 包,都已經支持 ESM,或者能夠比較好的被轉換成 ESM,因此也不需要太擔心 Vite 的問題。
“ESM與CJS互相轉換怎么實現”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。