Node的CJS與ESM有哪些不同點

今天小編給大家分享一下Node的CJS與ESM有哪些不同點的相關知識點,內(nèi)容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。

成都創(chuàng)新互聯(lián)公司-專業(yè)網(wǎng)站定制、快速模板網(wǎng)站建設、高性價比城東網(wǎng)站開發(fā)、企業(yè)建站全套包干低至880元,成熟完善的模板庫,直接使用。一站式城東網(wǎng)站制作公司更省心,省錢,快速模板網(wǎng)站建設找我們,業(yè)務覆蓋城東地區(qū)。費用合理售后完善,10余年實體公司更值得信賴。

Node的CJS與ESM有哪些不同點

一、并不完美的 ESM 支持

1.1 在 Node 中使用 ESM

Node 默認只支持 CJS 語法,這意味著你書寫了一個 ESM 語法的 js 文件,將無法被執(zhí)行。

如果想在 Node 中使用 ESM 語法,有兩種可行方式:

  • ⑴ 在 package.json 中新增 "type": "module" 配置項。

  • ⑵ 將希望使用 ESM 的文件改為 .mjs 后綴。

對于第一種方式,Node 會將和 package.json 文件同路徑下的模塊,全部當作 ESM 來解析。

第二種方式不需要修改 package.json,Node 會自動地把全部 xxx.mjs 文件都作為 ESM 來解析。

同理,如果在 package.json 文件中設置 "type": "commonjs",則表示該路徑下模塊以 CJS 形式來解析。 如果文件后綴名為 .cjs,Node 會自動地將其作為 CJS 模塊來解析(即使在 package.json 中配置為 ESM 模式)。

我們可以通過上述修改 package.json 的方式,來讓全部模塊都以 ESM 形式執(zhí)行,然后項目上的模塊都統(tǒng)一使用 ESM 語法來書寫。

如果存在較多陳舊的 CJS 模塊懶得修改,也沒關系,把它們?nèi)颗驳揭粋€文件夾,在該文件夾路徑下新增一個內(nèi)容為 {"type": "commonjs"}package.json 即可。

Node 在解析某個被引用的模塊時(無論它是被 import 還是被 require),會根據(jù)被引用模塊的后綴名,或對應的 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í)行

1.3 CJS 引用 ESM 模塊的問題

假設你在開發(fā)一個供別人使用的開源項目,且使用 ESM 的形式導出模塊,那么問題來了 —— 目前 CJS 的 require 函數(shù)無法直接引入 ESM 包,會報錯:

let { foo } = require('./esm/b.js');
              ^

Error [ERR_REQUIRE_ESM]: require() of ES Module BlogDemo3\220220\test2\esm\b.js from BlogDemo3\220220\test2\require.js not supported.
Instead change the require of b.js in BlogDemo3\220220\test2\require.js to a dynamic import() which is available in all CommonJS modules.
    at Object.<anonymous> (BlogDemo3\220220\test2\require.js:4:15) {
  code: 'ERR_REQUIRE_ESM'
}

按照上述錯誤陳述,我們不能并使用 require 引入 ES 模塊(原因會在后續(xù)提及),應當改為使用 CJS 模塊內(nèi)置的動態(tài) import 方法:

import('./esm/b.js').then(({ foo }) => {
    foo();
});

// or

(async () => { 
    const { foo } = await import('./esm/b.js'); 
})();

開源項目當然不能強制要求用戶改用這種形式來引入,所以又得借助 rollup 之類的工具將項目編譯為 CJS 模塊……


由上可見目前 Node.js 對 ESM 語法的支持是有限制的,如果不借助工具處理,這些限制可能會很糟心。

對于想入門前端的新手來說,這些麻煩的規(guī)則和限制也會讓人困惑。

截至我落筆書寫本文時, Node.js LTS 版本為 16.14.0,距離開始支持 ESM 的 13.2.0 版本已過去了兩年多的時間。

那么為何 Node.js 到現(xiàn)在還無法打通 CJS 和 ESM?

答案并非 Node.js 敵視 ESM 標準從而遲遲不做優(yōu)化,而是因為 —— CJS 和 ESM,二者真是太不一樣了。

二、CJS 和 ESM 的不同點

2.1 不同的加載邏輯

在 CJS 模塊中,require() 是一個同步接口,它會直接從磁盤(或網(wǎng)絡)讀取依賴模塊并立即執(zhí)行對應的腳本。

ESM 標準的模塊加載器則完全不同,它讀取到腳本后不會直接執(zhí)行,而是會先進入編譯階段進行模塊解析,檢查模塊上調用了 importexport 的地方,并順騰摸瓜把依賴模塊一個個異步、并行地下載下來。

在此階段 ESM 加載器不會執(zhí)行任何依賴模塊代碼,只會進行語法檢錯、確定模塊的依賴關系、確定模塊輸入和輸出的變量。

最后 ESM 會進入執(zhí)行階段,按順序執(zhí)行各模塊腳本。

所以我們常常會說,CommonJS 模塊是運行時加載,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();

這是因為 ESM 獲取所指定的依賴模塊屬性(花括號內(nèi)部的屬性),是需要在編譯階段進行靜態(tài)分析的,而 CJS 的腳本要在執(zhí)行階段才能計算出它們的 named exports 的值,會導致 ESM 在編譯階段無法進行分析。

2.2 不同的模式

ESM 默認使用了嚴格模式(use strict),因此在 ES 模塊中的 this 不再指向全局對象(而是 undefined),且變量在聲明前無法使用。

這也是為何在瀏覽器中,<script> 標簽如要啟用原生引入 ES 模塊能力,必須加上 type="module" 告知瀏覽器應當把它和常規(guī) JS 區(qū)分開來處理。

查看 ESM 嚴格模式的更多限制:

https://es6.ruanyifeng.com/#docs/module#%E4%B8%A5%E6%A0%BC%E6%A8%A1%E5%BC%8F

2.3 ESM 支持“頂級 await”,但 CJS 不行。

ESM 支持頂級 await(top-level await),即 ES 模塊中,無須在 async 函數(shù)內(nèi)部就能直接使用 await

// index.mjs
const { foo } = await import('./c.js');
foo();

到 Github 獲取示例代碼(test3):

https://github.com/VaJoy/BlogDemo3/tree/main/220220/test3

在 CSJ 模塊中是沒有這種能力的(即使使用了動態(tài)的 import 接口),這也是為何 require 無法加載 ESM 的原因之一。

試想一下,一個 CJS 模塊里的 require 加載器同步地加載了一個 ES 模塊,該 ES 模塊里異步地 import 了一個 CJS 模塊,該 CJS 模塊里又同步地去加載一個 ES 模塊…… 這種復雜的嵌套邏輯處理起來會變得十分棘手。

查閱關于更多“如何實現(xiàn) require 加載 ESM”的討論:

https://github.com/nodejs/modules/issues/454

2.4 ESM 缺乏 __filename 和 __dirname

在 CJS 中,模塊的執(zhí)行需要用函數(shù)包起來,并指定一些常用的值:

  NativeModule.wrapper = [
    '(function (exports, require, module, __filename, __dirname) { ',
    '\n});'
  ];

所以我們才可以在 CJS 模塊里直接用 __filename__dirname。

而 ESM 的標準中不包含這方面的實現(xiàn),即無法在 Node 的 ESM 里使用 __filename__dirname。

從上方幾點可以看出,在 Node.js 中,如果要把默認的 CJS 切換到 ESM,會存在巨大的兼容性問題。

這也是 Node.js 目前,甚至未來很長一段時間,都難以解決的一場模塊規(guī)范持久戰(zhàn)。

如果你希望不借助工具和規(guī)則,也能放寬心地使用 ESM,可以嘗試使用 Deno 替代 Node,它默認采用了 ESM 作為模塊規(guī)范(當然生態(tài)沒有 Node 這么完善)。

三、借助工具實現(xiàn) CJS、ESM 混寫

借助構建工具可以實現(xiàn) CJS 模塊、ES 模塊的混用,甚至可以在同一個模塊同時混寫兩種規(guī)范的 API,讓開發(fā)不再需要關心 Node.js 上面的限制。另外構建工具還能利用 ESM 在編譯階段靜態(tài)解析的特性,實現(xiàn) Tree-shaking 效果,減少冗余代碼的輸出。

這里我們以 rollup 為例,先做全局安裝:

pnpm i -g rollup

接著再安裝 rollup-plugin-commonjs 插件,該插件可以讓 rollup 支持引入 CJS 模塊(rollup 本身是不支持引入 CJS 模塊的):

pnpm i --save-dev @rollup/plugin-commonjs

我們在項目根目錄新建 rollup 配置文件 rollup.config.js

import commonjs from 'rollup-plugin-commonjs';

export default {
  input: 'index.js',  // 入口文件
  output: {
    file: 'bundle.js',  // 目標文件
    format: 'iife'
  },
  plugins: [
    commonjs({
      transformMixedEsModules: true,
      sourceMap: false,
    })
  ]
};

plugin-commonjs 默認會跳過所有含 import/export 的模塊,如果要支持如 import + require 的混合寫法,需要帶 transformMixedEsModules 屬性。

接著執(zhí)行 rollup --config 指令,就能按照 rollup.config.js 進行編譯和打包了。

示例

/** @file a.js **/
export let func = () => {
    console.log("It's an a-func...");
}

export let deadCode = () => {
    console.log("[a.js deadCode] Never been called here");
}


/** @file b.js **/
// named exports
module.exports = {
    func() {
        console.log("It's a b-func...")
    },
    deadCode() {
        console.log("[b.js deadCode] Never been called here");
    }
}


/** @file c.js **/
module.exports.func = () => {
    console.log("It's a c-func...")
};

module.exports.deadCode = () => {
    console.log("[c.js deadCode] Never been called here");
}


/** @file index.js **/
let a = require('./a');
import { func as bFunc } from './b.js';
import { func as cFunc } from './c.js';

a.func();
bFunc();
cFunc();

打包后的 bundle.js 文件如下:

(function () {
	'use strict';

	function getAugmentedNamespace(n) {
		if (n.__esModule) return n;
		var a = Object.defineProperty({}, '__esModule', {value: true});
		Object.keys(n).forEach(function (k) {
			var d = Object.getOwnPropertyDescriptor(n, k);
			Object.defineProperty(a, k, d.get ? d : {
				enumerable: true,
				get: function () {
					return n[k];
				}
			});
		});
		return a;
	}

	let func$1 = () => {
	    console.log("It's an a-func...");
	};

	let deadCode = () => {
	    console.log("[a.js deadCode] Never been called here");
	};

	var a$1 = /*#__PURE__*/Object.freeze({
		__proto__: null,
		func: func$1,
		deadCode: deadCode
	});

	var require$$0 = /*@__PURE__*/getAugmentedNamespace(a$1);

	var b = {
	    func() {
	        console.log("It's a b-func...");
	    },
	    deadCode() {
	        console.log("[b.js deadCode] Never been called here");
	    }
	};

	var func = () => {
	    console.log("It's a c-func...");
	};

	let a = require$$0;

	a.func();
	b.func();
	func();

})();

可以看到,rollup 通過 Tree-shaking 移除掉了從未被調用過的 c 模塊的 deadCode 方法,但 a、b 兩模塊中的 deadCode 代碼段未被移除,這是因為我們在引用 a.js 時使用了 require,在 b.js 中使用了 CJS named exports,這些都導致了 rollup 無法利用 ESM 的特性去做靜態(tài)解析。

常規(guī)在開發(fā)項目時,還是建議盡量使用 ESM 的語法來書寫全部模塊,這樣可以最大化地利用構建工具來減少最終構建文件的體積。

以上就是“Node的CJS與ESM有哪些不同點”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關注創(chuàng)新互聯(lián)行業(yè)資訊頻道。

當前題目:Node的CJS與ESM有哪些不同點
URL地址:http://bm7419.com/article28/gihdjp.html

成都網(wǎng)站建設公司_創(chuàng)新互聯(lián),為您提供建站公司、外貿(mào)網(wǎng)站建設App設計、網(wǎng)站設計品牌網(wǎng)站建設、品牌網(wǎng)站設計

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉載內(nèi)容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉載,或轉載時需注明來源: 創(chuàng)新互聯(lián)

網(wǎng)站優(yōu)化排名