對比ECMAScript 模塊 (ESM) 和 CommonJS
JavaScript 最初只是一種簡單的語言,用于使靜態網站更具活力和交互性。然而,用 JavaScript 編寫的項目在很久以前就開始變得越來越復雜。
正因為如此,我們很快就發現需要一種方法來將代碼分解成更小、更易于管理的片段。
多年來,人們對如何實現將 JavaScript 代碼拆分成模塊有很多不同的想法。在本文中,我們將比較最流行的幾種方法:ECMAScript 模塊 (ESM) 和 CommonJS。
CommonJS
Node.js 的創建者知道,代碼需要組織成可重用的模塊。然而,當 Node.js 于 2009 年首次推出時,JavaScript 還沒有正式的模塊系統。因此,Node.js 引入了 CommonJS 模塊。
創建和導出模塊
在 Node.js 中,每個文件都是一個獨立的模塊。要開始使用 CommonJS,讓我們用一個簡單的函數創建一個新文件。
sum.js
function sum(numberOne, numberTwo) {
return numberOne + numberTwo;
}
module.exports = { sum };
在每個文件中,module 變量都代表當前模塊。我們可以使用 module.exports 來讓其他模塊導入和使用 sum 函數。
導入模塊
要導入模塊,我們需要使用 require 函數并提供正確的路徑。
const { sum } = require('./sum');
console.log(sum(1, 2)); // 3
CommonJS 還支持 import 函數,它允許我們異步導入模塊。
import('./sum.js')
.then(({ sum }) => {
console.log(sum(1, 2)); // 3
})
ECMAScript Modules
JavaScript 語言進行了一次重大升級,即 ES6 或 ECMAScript 2015。除其他功能外,它還包括模塊管理的官方語法,即 ECMAScript Modules (ESM)。
創建和導出模塊
我們必須使用導出關鍵字來公開模塊中的各種值。
sum.js
function sum(numberOne, numberTwo) {
return numberOne + numberTwo;
}
export { sum };
使用上述語法,我們可以導出任意多個值。此外,模塊還可以包含一個默認導出。
subtract.js:
function subtract(numberOne, numberTwo) {
return numberOne - numberTwo;
}
export default subtract;
導入和導出模塊
要導入 ECMAScript 模塊,我們必須使用 import 關鍵字。
import { sum } from './sum.js';
import subtract from './subtract.js';
console.log(sum(3, 2)); // 5
console.log(subtract(3, 2)); // 1
請注意,我們使用略有不同的語法來導入默認導出。重要的是,默認導入使用的名稱不必與默認導出一致。
import sum from './subtract.js';
這也是有些人不喜歡使用默認出口的原因之一。
ESM 還支持導入功能,允許我們異步導入模塊。
import('./sum.js')
.then(({ sum }) => {
console.log(sum(1, 2)); // 3
})
ECMAScript 模塊的兼容
雖然 ECMAScript 模塊是在 2015 年左右推出的,但社區還是花了一段時間才跟上步伐。不過,現在我們甚至可以在瀏覽器中原生使用 ESM。
webpack 等捆綁程序多年前就開始支持 ECMAScript 模塊。它們可以將我們在多個 JavaScript 文件中使用 ESM 的代碼轉換成單個文件輸出。它還可以將捆綁包拆分成多個文件,以提高性能。
Node.js 在 8.5.0 版左右開始嘗試支持 ESM。不過當時要使用它,我們必須包含 --experimental-modules 模塊。在 13.2.0 版中,他們取消了在使用 Node 模塊時使用該標志的要求。
不過,在 Node.js 中使用 ESM 仍然會在終端中出現警告,提示該功能是試驗性的。自 2020 年 4 月發布 14.0.0 版后,該警告不再出現。
要在 Node.js 中使用 ESM,我們可以在 package.js 中添加 "type"(類型)和 "module"(模塊):"模塊"。
有趣的是,如果您習慣于使用 Webpack 等工具,那么 Node.js 中的 ESM 實現可能會略有不同。我們必須提供導入模塊的完整路徑,包括文件擴展名。
TypeScript 中的 ECMAScript 模塊
早在 2015 年的 TypeScript 1.5 中,TypeScript 就開始支持 ECMAScript Modules 語法。需要了解的是,它默認將我們的代碼移植到使用 CommonJS 的引擎蓋下。
import { sum } from './sum';
sum(1, 2);
使用默認配置將上述文件轉換為 JavaScript 時,我們可以看到 CommonJS。
"use strict";
exports.__esModule = true;
var sum_1 = require("./sum");
(0, sum_1.sum)(1, 2);
TypeScript 添加了 __esModule 標記,以表明文件是從 ESM 編譯到 CommonJS 的。
要在 TypeScript 和 Node.js 中使用 ESM,我們需要稍微修改一下 tsconfig.json 文件。
tsconfig.json
{
"compilerOptions": {
"strict": true,
"module": "NodeNext",
"outDir": "dist"
}
}
由于使用了 NodeNext,TypeScript 將使用 ESM 導入和導出,而不是 CommonJS。請務必記住,Node.js 要求我們提供導入模塊的完整路徑。
當我們使用希望在 Node.js 環境中運行的 TypeScript 時,也必須這樣做。
與直覺相反的是,即使我們在編寫 TypeScript 代碼,也必須提供包含 .js 擴展名的路徑。
import { sum } from './sum.js';
console.log(sum(1, 2));
完成所有這些后,我們就可以擁有一個在 Node.js 中運行并在引擎蓋下使用 ECMAScript 模塊的 TypeScript 應用程序。
管理依賴關系
在處理依賴關系時,在 CommonJS 和 ECMAScript 模塊之間做出選擇的問題會變得更加復雜。
使用 ECMAScript 模塊的項目可以使用導入語法使用 CommonJS 模塊。但是,使用 CommonJS 的項目除了通過異步導入函數外,不能以任何方式導入僅使用 ESM 的模塊。
正因為如此,許多使用 ECMAScript 編寫 JavaScript 庫的開發人員決定同時發布 CommonJS 和 ESM 代碼。
這樣,他們的庫就可以兼容任何一種模塊系統。
然而,并非所有開發人員都愿意處理發布同時適用于 CommonJS 和 ESM 的軟件包的麻煩。
只發布與 ESM 兼容的庫版本的做法越來越流行。
因此,我們需要了解 CommonJS 和 ESM 的工作原理及其局限性。
總結
在本文中,我們介紹了 CommonJS 和 ECMAScript 模塊的工作原理。我們了解了它們的語法以及如何在 Node.js 和 TypeScript 中使用它們。
由于并非所有開發人員都愿意費盡周折地發布同時適用于 CommonJS 和 ESM 的包,因此了解這兩種模塊系統之間的區別變得越來越重要。
有了這些知識,我們才能更好地為項目選擇合適的模塊系統,并適應與之相關的任何挑戰。