如果您是 Node.js 開發人員,您可能聽說過 cjs 和 esm 模組,但可能不確定為什麼有兩個模組以及它們如何在 Node.js 應用程式中共存。這篇部落格文章將簡要介紹 Node.js 中 JavaScript 模組的歷史(附有範例?),以便您在處理這些概念時更加自信。
最初,JavaScript 僅具有全域作用域,所有成員都已聲明。共享程式碼時這是有問題的,因為兩個獨立的檔案可能對成員使用相同的名稱。例如:
greet-1.js
function greet(name) { return `Hello ${name}!`; }
greet-2.js
var greet = "...";
index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Collision example</title> </head> <body> <!-- After this script, `greet` is a function --> <script src="greet-1.js"></script> <!-- After this script, `greet` is a string --> <script src="greet-2.js"></script> <script> // TypeError: "greet" is not a function greet(); </script> </body> </html>
Node.js 透過 CommonJS(也稱為 cjs)正式引入了 JavaScript 模組的概念。這解決了共享全域範圍的衝突問題,因為開發人員可以決定要導出什麼(透過 module.exports)和導入(透過 require())。例如:
src/greet.js
// this remains "private" const GREETING_PREFIX = "Hello"; // this will be exported function greet(name) { return `${GREETING_PREFIX} ${name}!`; } // `exports` is a shortcut to `module.exports` exports.greet = greet;
src/main.js
// notice the `.js` suffix is missing const { greet } = require("./greet"); // logs: Hello Alice! console.log(greet("Alice"));
由於 npm 套件允許開發人員發布和使用可重複使用的 JavaScript 程式碼,Node.js 開發迅速流行。 npm 套件預設安裝在 node_modules 資料夾中。所有 npm 套件中存在的 package.json 檔案尤其重要,因為它可以透過「main」屬性指示 Node.js 哪個檔案是入口點。例如:
node_modules/greeter/package.json
{ "name": "greeter", "main": "./entry-point.js" // ... }
node_modules/greeter/entry-point.js
module.exports = { greet(name) { return `Hello ${name}!`; } };
src/main.js
// notice there's no relative path (e.g. `./`) const { greet } = require("greeter"); // logs: Hello Bob! console.log(greet("Bob"));
npm 套件能夠利用其他開發人員的工作,從而大大提高了開發人員的工作效率。然而,它有一個主要缺點:cjs 與網頁瀏覽器不相容。為了解決這個問題,捆綁器的概念誕生了。 browserify 是第一個捆綁器,本質上是透過遍歷入口點並將所有 require() 程式碼「捆綁」到與 Web 瀏覽器相容的單一 .js 檔案中來運作的。隨著時間的推移,其他具有附加功能和差異化因素的捆綁器也被引入。最值得注意的是 webpack、parcel、rollup、esbuild 和 vite(按時間順序排列)。
隨著 Node.js 和 cjs 模組成為主流,ECMAScript 規範維護者決定納入模組概念。這就是為什麼原生 JavaScript 模組也被稱為 ESModules 或 esm(ECMAScript 模組的縮寫)。
esm 定義了用於匯出和匯入成員的新關鍵字和語法,並引入了預設匯出等新概念。隨著時間的推移,esm 模組獲得了新的功能,例如動態 import() 和頂級等待。例如:
src/greet.js
function greet(name) { return `Hello ${name}!`; }
src/part.js
var greet = "...";
src/main.js
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Collision example</title> </head> <body> <!-- After this script, `greet` is a function --> <script src="greet-1.js"></script> <!-- After this script, `greet` is a string --> <script src="greet-2.js"></script> <script> // TypeError: "greet" is not a function greet(); </script> </body> </html>
隨著時間的推移,由於捆綁程式和 TypeScript 等語言,esm 被開發人員廣泛採用,因為它們能夠將 esm 語法轉換為 cjs。
由於需求不斷增長,Node.js 在 12.x 版本中正式添加了對 esm 的支援。與 cjs 的向後相容性實作如下:
當涉及 npm 套件相容性時,esm 模組可以使用 cjs 和 esm 入口點導入 npm 套件。然而,相反的情況也有一些警告。舉例:
node_modules/cjs/package.json
// this remains "private" const GREETING_PREFIX = "Hello"; // this will be exported function greet(name) { return `${GREETING_PREFIX} ${name}!`; } // `exports` is a shortcut to `module.exports` exports.greet = greet;
node_modules/cjs/entry.js
// notice the `.js` suffix is missing const { greet } = require("./greet"); // logs: Hello Alice! console.log(greet("Alice"));
node_modules/esm/package.json
{ "name": "greeter", "main": "./entry-point.js" // ... }
node_modules/esm/entry.js
module.exports = { greet(name) { return `Hello ${name}!`; } };
以下運作正常:
src/main.mjs
// notice there's no relative path (e.g. `./`) const { greet } = require("greeter"); // logs: Hello Bob! console.log(greet("Bob"));
但是,以下指令無法運作:
src/main.cjs
// this remains "private" const GREETING_PREFIX = "Hello"; // this will be exported export function greet(name) { return `${GREETING_PREFIX} ${name}!`; }
不允許這樣做的原因是因為 esm 模組允許頂層等待,而 require() 函數是同步的。可以重寫程式碼以使用動態 import(),但由於它會傳回一個 Promise,因此它強制具有如下所示的內容:
src/main.cjs
// default export: new concept export default function part(name) { return `Goodbye ${name}!`; }
為了緩解此相容性問題,一些 npm 套件透過利用帶有條件導出的 package.json 的「exports」屬性來公開 cjs 和 mjs 入口點。例如:
node_modules/esm/entry.cjs:
// notice the `.js` suffix is required import part from "./part.js"; // dynamic import: new capability // top-level await: new capability const { greet } = await import("./greet.js"); // logs: Hello Alice! console.log(greet("Alice")); // logs: Bye Bob! console.log(part("Bob"));
node_modules/esm/package.json:
{ "name": "cjs", "main": "./entry.js" }
注意「main」如何指向 cjs 版本,以便向後相容不支援「exports」屬性的 Node.js 版本。
這(幾乎)是您需要了解的有關 cjs 和 esm 模組的全部資訊(截至 2024 年 12 月?)。請在下面告訴我你的想法!
以上是Node.js:cjs、捆綁器和 esm 簡史的詳細內容。更多資訊請關注PHP中文網其他相關文章!