首页 >web前端 >js教程 >Node.js:cjs、捆绑器和 esm 简史

Node.js:cjs、捆绑器和 esm 简史

Mary-Kate Olsen
Mary-Kate Olsen原创
2024-12-15 05:36:10394浏览

Node.js: A brief history of cjs, bundlers, and esm

介绍

如果您是 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>

CommonJS 模块

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 包

由于 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(按时间顺序排列)。

ECMAScript 模块

随着 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 cjs/esm 互操作性

由于需求不断增长,Node.js 在 12.x 版本中正式添加了对 esm 的支持。与 cjs 的向后兼容性实现如下:

  • Node.js 将 .js 文件解释为 cjs 模块,除非 package.json 将“type”属性设置为“module”。
  • Node.js 将 .cjs 文件解释为 cjs 模块。
  • Node.js 将 .mjs 文件解释为 esm 模块。

当涉及到 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中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn