Skip to content

模块:ECMAScript 模块

[历史]

版本变更
v23.1.0import 属性不再是实验性功能。
v22.0.0取消对 import 断言的支持。
v21.0.0, v20.10.0, v18.20.0添加对 import 属性的实验性支持。
v20.0.0, v18.19.0模块定制钩子在主线程之外执行。
v18.6.0, v16.17.0添加对模块定制钩子链式调用的支持。
v17.1.0, v16.14.0添加对 import 断言的实验性支持。
v17.0.0, v16.12.0合并定制钩子,移除 getFormatgetSourcetransformSourcegetGlobalPreloadCode 钩子,添加 loadglobalPreload 钩子,允许从 resolveload 钩子返回 format
v14.8.0取消顶级 await 的实验性标记。
v15.3.0, v14.17.0, v12.22.0稳定模块实现。
v14.13.0, v12.20.0支持检测 CommonJS 具名导出。
v14.0.0, v13.14.0, v12.20.0移除实验性模块警告。
v13.2.0, v12.17.0加载 ECMAScript 模块不再需要命令行标志。
v12.0.0通过 package.json"type" 字段添加对使用 .js 文件扩展名的 ES 模块的支持。
v8.5.0在 v8.5.0 版本中添加

[稳定: 2 - 稳定]

稳定: 2 稳定性: 2 - 稳定

简介

ECMAScript 模块是 官方标准格式 ,用于打包 JavaScript 代码以供重用。模块使用各种 importexport 语句定义。

以下是一个导出函数的 ES 模块示例:

js
// addTwo.mjs
function addTwo(num) {
  return num + 2
}

export { addTwo }

以下是一个从 addTwo.mjs 导入函数的 ES 模块示例:

js
// app.mjs
import { addTwo } from './addTwo.mjs'

// 输出:6
console.log(addTwo(4))

Node.js 完全支持当前指定的 ECMAScript 模块,并提供它们与其原始模块格式 CommonJS 之间的互操作性。

启用

Node.js 具有两种模块系统:CommonJS 模块和 ECMAScript 模块。

作者可以通过 .mjs 文件扩展名、package.json"type" 字段(值为 "module")或 --input-type 标志(值为 "module")来告诉 Node.js 将 JavaScript 解释为 ES 模块。这些是代码旨在作为 ES 模块运行的明确标记。

反之,作者可以通过 .cjs 文件扩展名、package.json"type" 字段(值为 "commonjs")或 --input-type 标志(值为 "commonjs")来明确告诉 Node.js 将 JavaScript 解释为 CommonJS。

当代码缺乏任何模块系统的明确标记时,Node.js 将检查模块的源代码以查找 ES 模块语法。如果找到此类语法,Node.js 将代码作为 ES 模块运行;否则,它将模块作为 CommonJS 运行。有关更多详细信息,请参阅 确定模块系统

本节内容已移至 模块:包

import 说明符

术语

import 语句的 说明符from 关键字后的字符串,例如 import { sep } from 'node:path' 中的 'node:path'。说明符也用于 export from 语句,以及作为 import() 表达式的参数。

说明符有三种类型:

  • 相对说明符,例如 './startup.js''../config.mjs'。它们指的是相对于导入文件位置的路径。这些说明符始终需要文件扩展名。
  • 裸说明符,例如 'some-package''some-package/shuffle'。它们可以指代包的主入口点(通过包名),或者指代包内特定功能模块(通过包名前缀,如示例所示)。只有当包没有 "exports" 字段时,才需要包含文件扩展名。
  • 绝对说明符,例如 'file:///opt/nodejs/config.js'。它们直接且明确地指代完整路径。

裸说明符的解析由 Node.js 模块解析和加载算法 处理。所有其他说明符的解析始终仅使用标准相对 URL 解析语义。

与 CommonJS 一样,除非包的 package.json 包含 "exports" 字段,否则包中的模块文件可以通过附加路径到包名的方式访问;如果包含 "exports" 字段,则包中的文件只能通过 "exports" 中定义的路径访问。

有关适用于 Node.js 模块解析中裸说明符的这些包解析规则的详细信息,请参见 包文档

必需的文件扩展名

使用 import 关键字解析相对或绝对说明符时,必须提供文件扩展名。目录索引(例如 './startup/index.js')也必须完全指定。

此行为与浏览器环境中 import 的行为匹配,假设服务器配置典型。

URL

ES 模块作为 URL 解析和缓存。这意味着特殊字符必须进行 百分比编码,例如 # 使用 %23? 使用 %3F

支持 file:node:data: URL 方案。除非使用 自定义 HTTPS 加载器,否则 Node.js 本身不支持像 'https://example.com/app.js' 这样的说明符。

file: URL

如果用于解析模块的 import 说明符具有不同的查询或片段,则模块将被加载多次。

js
import './foo.mjs?query=1' // 加载 ./foo.mjs,查询为 "?query=1"
import './foo.mjs?query=2' // 加载 ./foo.mjs,查询为 "?query=2"

卷根目录可以通过 ///file:/// 来引用。鉴于 URL 和路径解析(例如百分比编码细节)之间的差异,建议在导入路径时使用 url.pathToFileURL

data: 导入

新增于:v12.10.0

使用以下 MIME 类型支持导入 data: URL

  • text/javascript 用于 ES 模块
  • application/json 用于 JSON
  • application/wasm 用于 Wasm
js
import 'data:text/javascript,console.log("hello!");';
import _ from 'data:application/json,"world!"' with { type: 'json' };

data: URL 仅解析内置模块的裸规范绝对规范。解析相对规范不起作用,因为 data: 不是特殊方案。例如,尝试从 data:text/javascript,import "./foo"; 加载 ./foo 会失败,因为 data: URL 没有相对解析的概念。

node: 导入

[历史]

版本变更
v16.0.0, v14.18.0require(...) 添加了 node: 导入支持。
v14.13.1, v12.20.0新增于:v14.13.1, v12.20.0

node: URL 作为加载 Node.js 内置模块的替代方法受支持。此 URL 方案允许通过有效的绝对 URL 字符串引用内置模块。

js
import fs from 'node:fs/promises'

导入属性

[历史]

版本变更
v21.0.0, v20.10.0, v18.20.0从导入断言切换到导入属性。
v17.1.0, v16.14.0新增于:v17.1.0, v16.14.0

[稳定:2 - 稳定]

稳定:2 稳定性:2 - 稳定

导入属性 是一种内联语法,用于模块导入语句,以便在模块说明符之外传递更多信息。

js
import fooData from './foo.json' with { type: 'json' };

const { default: barData } =
  await import('./bar.json', { with: { type: 'json' } });

Node.js 只支持 type 属性,它支持以下值:

属性 type需要用于
'json'JSON 模块

导入 JSON 模块时,type: 'json' 属性是必需的。

内建模块

内建模块 提供其公共 API 的命名导出。还提供一个默认导出,其值为 CommonJS 导出。默认导出可用于修改命名导出等操作。内建模块的命名导出只有通过调用 module.syncBuiltinESMExports() 才能更新。

js
import EventEmitter from 'node:events'
const e = new EventEmitter()
js
import { readFile } from 'node:fs'
readFile('./foo.txt', (err, source) => {
  if (err) {
    console.error(err)
  } else {
    console.log(source)
  }
})
js
import fs, { readFileSync } from 'node:fs'
import { syncBuiltinESMExports } from 'node:module'
import { Buffer } from 'node:buffer'

fs.readFileSync = () => Buffer.from('Hello, ESM')
syncBuiltinESMExports()

fs.readFileSync === readFileSync

import() 表达式

动态 import() 在 CommonJS 和 ES 模块中都受支持。在 CommonJS 模块中,它可以用来加载 ES 模块。

import.meta

import.meta 元属性是一个包含以下属性的对象。它仅在 ES 模块中受支持。

import.meta.dirname

新增于:v21.2.0, v20.11.0

[稳定性:1 - 实验性]

稳定性:1 稳定性:1。2 - 发布候选版本

import.meta.filename

新增于:v21.2.0, v20.11.0

[稳定性:1 - 实验性]

稳定性:1 稳定性:1。2 - 发布候选版本

import.meta.url

这与浏览器中定义的方式完全相同,提供当前模块文件的 URL。

这使得诸如相对文件加载之类的有用模式成为可能:

js
import { readFileSync } from 'node:fs'
const buffer = readFileSync(new URL('./data.proto', import.meta.url))

import.meta.resolve(specifier)

[历史]

版本变更
v20.6.0, v18.19.0不再依赖 --experimental-import-meta-resolve CLI 标志,除了非标准的 parentURL 参数。
v20.6.0, v18.19.0当目标 file: URL 未映射到本地文件系统上的现有文件时,此 API 不会再抛出异常。
v20.0.0, v18.19.0此 API 现在同步返回字符串而不是 Promise。
v16.2.0, v14.18.0parentURL 参数添加对 WHATWG URL 对象的支持。
v13.9.0, v12.16.2新增于:v13.9.0, v12.16.2

[稳定性:1 - 实验性]

稳定性:1 稳定性:1.2 - 发布候选版本

  • specifier <字符串> 相对于当前模块解析的模块说明符。
  • 返回值: <字符串> 说明符将解析到的绝对 URL 字符串。

import.meta.resolve 是一个模块相关的解析函数,作用域为每个模块,返回 URL 字符串。

js
const dependencyAsset = import.meta.resolve('component-lib/asset.css')
// file:///app/node_modules/component-lib/asset.css
import.meta.resolve('./dep.js')
// file:///app/dep.js

Node.js 模块解析的所有功能都受支持。依赖项解析受包内允许的导出解析的约束。

警告:

  • 这可能导致同步文件系统操作,这可能会像 require.resolve 一样影响性能。
  • 此功能在自定义加载器中不可用(这会造成死锁)。

非标准 API:

使用 --experimental-import-meta-resolve 标志时,该函数接受第二个参数:

  • parent <字符串> | <URL> 一个可选的绝对父模块 URL,从中解析。默认值: import.meta.url

与 CommonJS 的互操作性

import 语句

import 语句可以引用 ES 模块或 CommonJS 模块。import 语句仅允许在 ES 模块中使用,但在 CommonJS 中支持动态 import() 表达式来加载 ES 模块。

导入 CommonJS 模块 时,module.exports 对象将作为默认导出提供。命名导出可能可用,通过静态分析提供,作为提高生态系统兼容性的便利措施。

require

CommonJS 模块 require 目前仅支持加载同步 ES 模块(即,不使用顶级 await 的 ES 模块)。

详情请参见 使用 require() 加载 ECMAScript 模块

CommonJS 命名空间

[历史]

版本变更
v23.0.0向 CJS 命名空间添加了 'module.exports' 导出标记。
v14.13.0添加于:v14.13.0

CommonJS 模块由一个 module.exports 对象组成,该对象可以是任何类型。

为了支持这一点,当从 ECMAScript 模块导入 CommonJS 时,将为 CommonJS 模块构建一个命名空间包装器,该包装器始终提供一个指向 CommonJS module.exports 值的 default 导出键。

此外,将对 CommonJS 模块的源文本执行启发式静态分析,以获得命名空间中值的最佳静态导出列表。这是必要的,因为这些命名空间必须在 CJS 模块评估之前构建。

这些 CommonJS 命名空间对象还将 default 导出作为 'module.exports' 命名导出提供,以便明确指示它们在 CommonJS 中的表示使用此值,而不是命名空间值。这反映了在 require(esm) 互操作性支持中处理 'module.exports' 导出名称的语义。

导入 CommonJS 模块时,可以使用 ES 模块默认导入或其对应的简写语法可靠地导入它:

js
import { default as cjs } from 'cjs'
// 与上面相同
import cjsSugar from 'cjs'

console.log(cjs)
console.log(cjs === cjsSugar)
// 打印:
//   <module.exports>
//   true

当使用 import * as m from 'cjs' 或动态导入时,可以直接观察到此模块命名空间奇异对象:

js
import * as m from 'cjs'
console.log(m)
console.log(m === (await import('cjs')))
// 打印:
//   [Module] { default: <module.exports>, 'module.exports': <module.exports> }
//   true

为了更好地与 JS 生态系统中现有的用法兼容,Node.js 还会尝试确定每个导入的 CommonJS 模块的 CommonJS 命名导出,以便使用静态分析过程将它们作为单独的 ES 模块导出提供。

例如,考虑一个编写的 CommonJS 模块:

js
// cjs.cjs
exports.name = 'exported'

前面的模块在 ES 模块中支持命名导入:

js
import { name } from './cjs.cjs'
console.log(name)
// 打印:'exported'

import cjs from './cjs.cjs'
console.log(cjs)
// 打印:{ name: 'exported' }

import * as m from './cjs.cjs'
console.log(m)
// 打印:
//   [Module] {
//     default: { name: 'exported' },
//     'module.exports': { name: 'exported' },
//     name: 'exported'
//   }

从最后列出的模块命名空间奇异对象的示例可以看出,当导入模块时,name 导出将从 module.exports 对象复制并直接设置在 ES 模块命名空间上。

对于这些命名导出,不会检测实时绑定更新或添加到 module.exports 的新导出。

命名导出的检测基于常见的语法模式,但并不总是能正确检测命名导出。在这些情况下,使用上面描述的默认导入形式可能是一个更好的选择。

命名导出检测涵盖许多常见的导出模式、重新导出模式以及构建工具和转译器输出。有关实现的确切语义,请参见 cjs-module-lexer

ES 模块和 CommonJS 的区别

没有 requireexportsmodule.exports

在大多数情况下,ES 模块的 import 可以用来加载 CommonJS 模块。

如果需要,可以使用 module.createRequire() 在 ES 模块中构造一个 require 函数。

没有 __filename__dirname

这些 CommonJS 变量在 ES 模块中不可用。

__filename__dirname 的用例可以通过 import.meta.filenameimport.meta.dirname 来复制。

没有加载 Addon

目前 ES 模块导入不支持Addon

它们可以使用 module.createRequire()process.dlopen 来加载。

没有 require.resolve

相对解析可以通过 new URL('./local', import.meta.url) 来处理。

对于完整的 require.resolve 替换,可以使用 import.meta.resolve API。

或者可以使用 module.createRequire()

没有 NODE_PATH

NODE_PATH 不参与解析 import 说明符。如果需要此行为,请使用符号链接。

没有 require.extensions

import 不使用 require.extensions。模块自定义钩子可以提供替代方案。

没有 require.cache

import 不使用 require.cache,因为 ES 模块加载器有其自己的独立缓存。

JSON 模块

[历史]

版本变更
v23.1.0JSON 模块不再是实验性的。

[稳定: 2 - 稳定]

稳定: 2 稳定性: 2 - 稳定

可以使用 import 引用 JSON 文件:

js
import packageConfig from './package.json' with { type: 'json' };

with { type: 'json' } 语法是必需的;请参阅 导入属性

导入的 JSON 只公开一个 default 导出。不支持命名导出。在 CommonJS 缓存中创建缓存条目以避免重复。如果 JSON 模块已从相同的路径导入,则在 CommonJS 中返回相同的对象。

Wasm 模块

[稳定版: 1 - 实验性]

稳定版: 1 稳定性: 1 - 实验性

--experimental-wasm-modules 标志下支持导入 WebAssembly 模块,允许将任何 .wasm 文件作为普通模块导入,同时还支持其模块导入。

此集成符合 WebAssembly 的 ES 模块集成提案

例如,一个包含以下内容的 index.mjs 文件:

js
import * as M from './module.wasm'
console.log(M)

在以下命令下执行:

bash
node --experimental-wasm-modules index.mjs

将提供 module.wasm 实例化的导出接口。

顶级 await

新增于: v14.8.0

await 关键字可以在 ECMAScript 模块的顶级主体中使用。

假设一个 a.mjs 文件包含:

js
export const five = await Promise.resolve(5)

以及一个 b.mjs 文件包含:

js
import { five } from './a.mjs'

console.log(five) // 输出 `5`
bash
node b.mjs # 可运行

如果顶级 await 表达式永不解析,则 node 进程将以 13状态码 退出。

js
import { spawn } from 'node:child_process'
import { execPath } from 'node:process'

spawn(execPath, [
  '--input-type=module',
  '--eval',
  // 永不解析的 Promise:
  'await new Promise(() => {})',
]).once('exit', code => {
  console.log(code) // 输出 `13`
})

加载器

之前的加载器文档现已移至模块:自定义钩子

解析和加载算法

特性

默认解析器具有以下属性:

  • 基于文件 URL 的解析,与 ES 模块使用方式相同
  • 相对和绝对 URL 解析
  • 没有默认扩展名
  • 没有文件夹主文件
  • 通过 node_modules 进行裸规范程序包解析查找
  • 不会因未知扩展名或协议而失败
  • 可以选择性地向加载阶段提供格式提示

默认加载器具有以下属性:

  • 通过 node: URL 支持内置模块加载
  • 通过 data: URL 支持“内联”模块加载
  • 支持 file: 模块加载
  • 对于任何其他 URL 协议都会失败
  • 对于 file: 加载的未知扩展名会失败(仅支持 .cjs.js.mjs

解析算法

加载 ES 模块规范的算法通过下面的 ESM_RESOLVE 方法给出。它返回相对于 parentURL 的模块规范的已解析 URL。

解析算法确定模块加载的完整已解析 URL 及其建议的模块格式。解析算法不确定是否可以加载已解析的 URL 协议,或者是否允许文件扩展名,而是 Node.js 在加载阶段应用这些验证(例如,如果它被要求加载具有非 file:data:node: 协议的 URL)。

该算法还尝试根据扩展名确定文件的格式(参见下面的 ESM_FILE_FORMAT 算法)。如果它不识别文件扩展名(例如,如果不是 .mjs.cjs.json),则返回 undefined 格式,这将在加载阶段引发错误。

确定已解析 URL 的模块格式的算法由 ESM_FILE_FORMAT 提供,它返回任何文件的唯一模块格式。对于 ECMAScript 模块,返回 "module" 格式,而 "commonjs" 格式用于指示通过旧版 CommonJS 加载器进行加载。诸如 "addon" 之类的附加格式可以在将来的更新中扩展。

在下述算法中,除非另有说明,否则所有子程序错误都将作为这些顶级例程的错误传播。

defaultConditions 是条件环境名称数组,["node", "import"]

解析器可能会抛出以下错误:

  • 无效的模块规范:模块规范是无效的 URL、包名称或包子路径规范。
  • 无效的包配置:package.json 配置无效或包含无效配置。
  • 无效的包目标:包导出或导入为包定义了一个目标模块,该目标模块是无效类型或字符串目标。
  • 包路径未导出:包导出未定义或不允许为给定模块在包中使用目标子路径。
  • 包导入未定义:包导入未定义规范。
  • 模块未找到:请求的包或模块不存在。
  • 不支持的目录导入:已解析的路径对应于目录,这不是模块导入的支持目标。

解析算法规范

ESM_RESOLVE(规范符, 父 URL)

PACKAGE_RESOLVE(包规范符, 父 URL)

PACKAGE_SELF_RESOLVE(包名, 包子路径, 父 URL)

PACKAGE_EXPORTS_RESOLVE(包 URL, 子路径, 导出, 条件)

PACKAGE_IMPORTS_RESOLVE(规范符, 父 URL, 条件)

PACKAGE_IMPORTS_EXPORTS_RESOLVE(匹配键, 匹配对象, 包 URL, 是否为导入, 条件)

PATTERN_KEY_COMPARE(键 A, 键 B)

PACKAGE_TARGET_RESOLVE(包 URL, 目标, 模式匹配, 是否为导入, 条件)

ESM_FILE_FORMAT(URL)

LOOKUP_PACKAGE_SCOPE(URL)

READ_PACKAGE_JSON(包 URL)

DETECT_MODULE_SYNTAX(源码)

自定义 ESM 规范符解析算法

模块自定义钩子 提供了一种自定义 ESM 规范符解析算法的机制。一个为 ESM 规范符提供 CommonJS 风格解析的示例是 commonjs-extension-resolution-loader