如果您是 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中文网其他相关文章!