中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Node的CJS與ESM有哪些不同點

發布時間:2022-03-04 09:40:54 來源:億速云 閱讀:168 作者:iii 欄目:web開發

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

Node的CJS與ESM有哪些不同點

一、并不完美的 ESM 支持

1.1 在 Node 中使用 ESM

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

如果想在 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 形式執行,然后項目上的模塊都統一使用 ESM 語法來書寫。

如果存在較多陳舊的 CJS 模塊懶得修改,也沒關系,把它們全部挪到一個文件夾,在該文件夾路徑下新增一個內容為 {"type": "commonjs"}package.json 即可。

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


1.3 CJS 引用 ESM 模塊的問題

假設你在開發一個供別人使用的開源項目,且使用 ESM 的形式導出模塊,那么問題來了 —— 目前 CJS 的 require 函數無法直接引入 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 模塊(原因會在后續提及),應當改為使用 CJS 模塊內置的動態 import 方法:

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

// or

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


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


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

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

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

那么為何 Node.js 到現在還無法打通 CJS 和 ESM?

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

二、CJS 和 ESM 的不同點

2.1 不同的加載邏輯

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

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

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

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

所以我們常常會說,CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口

在上方 1.2 小節,我們曾提及到 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 獲取所指定的依賴模塊屬性(花括號內部的屬性),是需要在編譯階段進行靜態分析的,而 CJS 的腳本要在執行階段才能計算出它們的 named exports 的值,會導致 ESM 在編譯階段無法進行分析。

2.2 不同的模式

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

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

查看 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 函數內部就能直接使用 await

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

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

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

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

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

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

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

2.4 ESM 缺乏 __filename 和 __dirname

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

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

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

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


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

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

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

三、借助工具實現 CJS、ESM 混寫

借助構建工具可以實現 CJS 模塊、ES 模塊的混用,甚至可以在同一個模塊同時混寫兩種規范的 API,讓開發不再需要關心 Node.js 上面的限制。另外構建工具還能利用 ESM 在編譯階段靜態解析的特性,實現 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 屬性。

接著執行 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 的特性去做靜態解析。

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

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

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

嘉黎县| 襄汾县| 稻城县| 双鸭山市| 栖霞市| 黎城县| 泰来县| 定远县| 泗洪县| 宿迁市| 岫岩| 麻城市| 涿鹿县| 贵溪市| 罗甸县| 瓦房店市| 上林县| 五常市| 嘉祥县| 绵竹市| 双江| 印江| 道真| 周至县| 和平区| 固安县| 海伦市| 涡阳县| 天长市| 宁津县| 寿宁县| 淮阳县| 浮梁县| 谢通门县| 翁源县| 怀远县| 陵川县| 红安县| 东乌| 山西省| 鹰潭市|