如何在 Node.js 中使用 ECMAScript 模块
在 ECMAScript 的大部分历史中,都缺少将代码打包为可重用模块的标准化方法。 在没有集成解决方案的情况下,CommonJS (CJS) 方法成为 Node.js 开发的标准。 使用 require 和 module.exports 来引入和导出代码片段:
// 导入一个模块 fs
const fs = require("fs");
// 提供一个导出代码
module.exports = () => "Hello World";
ES2015,也称为 ES6,终于引入了自己的内置模块系统。 ECMAScript 或 ES 模块 (ESM) 依赖于 import
和 export
语法:
// 导入默认的 export
import fs from "fs";
// 提供一个默认的 export
export default () => "Hello World";
// 导入一个命名的 export
import {helloWorld} from "./hello-world.js";
// 提供一个命名的 export
export const helloWorld = () => "Hello World";
Node.js 从 v16 开始就为 ESM 提供默认支持。 在早期版本中,我们需要使用 --experimental-modules
选项来激活该功能。 虽然 ES 模块现在被标记为稳定并可供使用,但存在两种不同的模块加载机制意味着在单个项目中使用这两种代码具有已定的困难性。
在本文中,我们将了解如何将 ES 模块与 Node 一起使用,以及如何最大限度地提高与 CommonJS 包的兼容性。
基础
如果正在开始一个新项目并希望依赖 ESM,则事情相对简单。 由于 Node.js 现在提供全面支持,我们可以将代码拆分为单独的文件并使用导入和导出语句来访问自己的模块。
不幸的是,我们确实需要尽早做出一些选择。 默认情况下,Node 不支持以 .js
扩展名结尾的内部文件的导入和导出。 可以将文件后缀设为 .mjs
,其中 ESM 始终可用,或者修改 package.json 文件使其包含 "type":"module"
。
{
"name": "example-package",
"type": "module",
"dependencies": {
"..."
}
}
对于专门使用 ESM 的项目,选择后一种方法通常更方便。 Node 使用 type 字段来确定项目的默认模块系统。 该模块系统始终用于处理普通的 .js
文件。 当不手动设置时,CJS 是默认的模块系统,以最大限度地与现有的 Node 代码生态系统兼容。 具有 .cjs
或 .mjs
扩展名的文件将始终分别被视为 CJS 和 ESM 格式的源码。
从 ESM 导入 CommonJS 模块
可以使用常规导入语句在 ESM 文件中导入 CJS 模块:
// cjs-module.cjs
module.exports.helloWorld = () => console.log("Hello World");
// esm-module.mjs
import component from "./cjs-module.cjs";
component.helloWorld();
组件将被解析为 CJS 模块的 module.exports 的值。 上面的示例显示了如何访问命名导出作为导入名称上的对象属性。 我们还可以使用 ESM 命名的导入语法访问特定的导出:
import {helloWorld} from "./cjs-module.cjs";
helloWorld();
这通过扫描 CJS 文件来计算它们提供的导出的静态分析系统来工作。 这种方法是必要的,因为 CJS 不理解“命名导出”的概念。 所有 CJS 模块都有一个导出——“命名”导出实际上是一个具有多个属性值对的对象。
由于静态分析过程的性质,可能无法正确检测到一些不常见的语法模式。 如果发生这种情况,我们将不得不通过默认导出对象访问我们需要的属性。
从 CJS 导入 ESM 模块
当我们想在现有 CJS 代码中使用新的 ESM 模块时,事情会变得更加棘手。 我们不能在 CJS 文件中使用 import 语句。 然而动态 import() 语法确实有效,并且可以与 await 配合从而相对方便地访问模块:
// esm-module.mjs
const helloWorld = () => console.log("Hello World");
export {helloWorld};
// esm-module-2.mjs
export default = () => console.log("Hello World");
// cjs-module.cjs
const loadHelloWorld = async () => {
const {helloWorld} = await import("./esm-module.mjs");
return helloWorld;
};
const helloWorld = await loadHelloWorld();
helloWorld();
const loadHelloWorld2 = async() => {
const helloWorld2 = await import("./esm-module-2.mjs");
return helloWorld2;
};
const helloWorld2 = await loadHelloWorld2();
helloWorld2();
此结构可用于异步访问 ESM 模块的默认导出和命名导出。
使用 ES 模块检索当前模块的路径
ES 模块无法访问 CJS 上下文中所有熟悉的 Node.js 全局变量。 除了 require() 和 module.exports,我们也不能访问 __dirname
或 __filename
常量。 这些通常由需要知道自己文件路径的 CJS 模块使用。
ESM 文件可以读取 import.meta.url 来获取此信息:
console.log(import.meta.url);
// file:///home/demo/module.mjs
返回的 URL 给出了当前文件的绝对路径。
为什么不兼容所有的?
CJS 和 ESM 之间的差异比简单的语法变化要深刻得多。 CJS是一个同步系统; 当 require() 一个模块时,Node 直接从磁盘加载它并执行它的内容。 ESM 是异步的,并将脚本导入拆分为几个不同的阶段。 导入被解析,从它们的存储位置异步加载,然后在以相同方式检索到它们自己的所有导入后执行。
ESM 被设计为具有广泛应用的现代模块加载解决方案。 这就是 ESM 模块适合在 Web 浏览器中使用的原因:它们在设计上是异步的,所以慢速网络不是问题。
ESM 的异步特性也导致了它在 CJS 代码中的使用限制。 CJS 文件不支持顶级 await,因此我们不能单独使用 import:
// this...
import component from "component.mjs";
// ...可以看成等价于这个...
const component = await import("component.mjs");
// ...但是 CJS 中没有顶级“ await”
因此,我们必须在异步函数中使用动态 import() 结构。
你应该切换到 ES 模块吗?
简单的答案是肯定的。 ES 模块是导入和导出 JavaScript 代码的标准化方式。 当语言缺乏自己的模块系统时,CJS 给了 Node 一个模块系统。 现在可用,最好采用 ECMAScript 标准描述的方法,以确保社区的长期健康。
ESM 也是更强大的系统。 因为它是异步的,所以我们可以获得动态导入、从 URL 远程导入,并在某些情况下提高了性能。 我们也可以在其他 JavaScript 运行时重用自己的模块,例如通过
相关文章
Node.js 中的 HTTP 发送 POST 请求
发布时间:2023/03/27 浏览次数:200 分类:Node.js
-
在本文中,我们将学习如何使用 Node.js 使用第三方包发出发送 post 请求。
Node.js 与 React JS 的比较
发布时间:2023/03/27 浏览次数:137 分类:Node.js
-
本文比较和对比了两种编程语言,Node.js 和 React。React 和 Node.js 都是开源 JavaScript 库的示例。 这些库用于构建用户界面和服务器端应用程序。