ES Modules(ESM)是一种官方的用于模块化 JS 代码的方式。通过 import
和 export
语句来定义模块。
导入导出的例子:
// `addTwo.mjs`
function addTwo(num) {
return num + 2;
}
export { addTwo };
// `app.mjs`
import { addTwo } from './addTwo.mjs';
// Prints: 6
console.log(addTwo(4));
Node 完全支持 ESM ,并提供了与 CommonJS 交互的方式。
启用方式
- 通过
.mjs
扩展名来告诉 Node 这是一个 ES 模块; - 在
package.json
里指定"type": "module"
; - 使用
--input-type
修饰符,并赋值为"module"
; - 使用
--experimental-default-type
修饰符,并赋值为"module"
。
如果没有主动告诉 Node 使用 ESM ,Node 会自己在代码中检测 ESM 相关语法。如果 Node 检测到 ESM 语法,则会将其视为 ES 模块,反之视为 CommonJS 模块。
import
指定
有三种指定模块的方式:
- 相对引用。比如
'./startup.js'
、'../config.mjs'
; - 包名。比如
'some-package'
、'some-package/shuffle'
; - 绝对引用。比如
'file:///opt/nodejs/config.js'
。
与 CommonJS 一样,可以通过添加相对路径来访问包内的模块文件。但如果 package.json
里指定了 exports
字段,则只能访问其列出的文件路径。
必要的文件扩展名
当通过相对或绝对引用指定导入的模块时,必须带上文件的扩展名。目录的 index 文件同样也需要指明完整的文件名,比如 './startup/index.js'
。
URL
ES 模块会以 URL 的形式解析并缓存。这意味着一些特殊字符需要进行百分号转义,比如 #
转义为 %34
。
Node 支持 file:
、node:
、data:
形式的 URL 。而 https
并不原生支持,需要一个自定义的 HTTPS loader 。
file:
URL
如果一个模块带有不同的参数或片段,则会被重复加载:
import './foo.mjs?query=1'; // loads ./foo.mjs with query of "?query=1"
import './foo.mjs?query=2'; // loads ./foo.mjs with query of "?query=2"
卷轴目录可以通过 /
、//
、file:///
引用。考虑到文件路径和 URL 之间的区别,推荐用 url.pathToFileURL
来获取一个正确的引用路径。
data:
导入
data:
URLs 支持导入以下 MIME 类型:
text/javascript
- ES 模块application/json
- JSONapplication/wasm
- Wasm
import 'data:text/javascript,console.log("hello!");';
import _ from 'data:application/json,"world!"' with { type: 'json' };
data:
URLs only resolve bare specifiers for builtin modules
and absolute specifiers. Resolving
relative specifiers does not work because data:
is not a
special scheme. For example, attempting to load ./foo
from data:text/javascript,import "./foo";
fails to resolve because there
is no concept of relative resolution for data:
URLs.
node:
导入
node:
URLs are supported as an alternative means to load Node.js builtin
modules. This URL scheme allows for builtin modules to be referenced by valid
absolute URL strings.
import fs from 'node:fs/promises';
import 属性
Stability: 1.1 - Active development
内置模块
核心模块支持具名导出,也支持默认导出,默认导出会获得原先在 CommonJS 的 export
对象。默认导出可以用来修改 API ,但需要调用 module.syncBuiltinESMExports
来应用更新:
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()
表达式
CommonJS 和 ESM 均支持动态 import()
调用。在 CommonJS 可以通过 import()
来引入 ES 模块。
import.meta
import.meta
包含以下属性:
import.meta.dirname
Stability: 1.2 - Release candidate
import.meta.filename
Stability: 1.2 - Release candidate
import.meta.url
- {string} 模块的
file:
URL 。
可以方便加载相对路径的文件模块:
import { readFileSync } from 'node:fs';
const buffer = readFileSync(new URL('./data.proto', import.meta.url));
import.meta.resolve(specifier)
Stability: 1.2 - Release candidate
与 CommonJS 交互
import
语句
import
语句只能在 ESM 中使用,但是可以用来导入 ESM 或 CommonJS 模块。而 CommonJS 模块可以用动态 import()
来导入 ESM 模块。
当导入 CommonJS 时,module.exports
将会作为默认导出。而具名导出并不完全是可用的,它依赖静态分析。
require
当 --experimental-require-module
开启时,CommonJS 模块可以同步加载 ES 模块。
See Loading ECMAScript modules using require()
for details.
CommonJS 命名空间
import { default as cjs } from 'cjs';
import cjsSugar from 'cjs';
console.log(cjs); // => <module.exports>
console.log(cjs === cjsSugar); // => true
CommonJS 模块在 ESM 中的 default
导出永远指向其 module.exports
值。
"Module Namespace Exotic Object" 可通过 import * as m from 'cjs'
或动态导入获取:
import * as m from 'cjs';
console.log(m); // => [Module] { default: <module.exports> }
console.log(m === await import('cjs')); // => true
为了兼容,Node 会通过静态解析给 CommonJS 模块提供具名导出。比如:
// cjs.cjs
exports.name = 'exported';
在 ESM 中支持该模块进行具名导入:
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' }, name: 'exported' }
在最后一行输出中可以看到 ES 模块的命名空间里存在一个 name
导出,它是从 moduels.exports
对象中复制而来的。
对 module.exports
的动态绑定更新不会同步到具名导出。
ESM 与 CommonJS 的不同
- 没有
require
、exports
、module.exports
。import
可以用来加载 CommonJS 模块;- 需要的时候,可以通过
module.createRequire
来构建一个require
函数。
- 没有
__filename
、__dirname
。 - 不支持 Addon 。不过可以通过
module.createRequire()
或process.dlopen
加载。 - 没有
require.resolve
。- 可以通过
new URL('./local', import.meta.url)
获取相对路径解析。
- 可以通过
- 没有
NODE_PATH
。 - 没有
require.extensions
。可以使用模块自定义勾子。 - 没有
require.cache
。ESM 有自己的缓存。
JSON 模块
Stability: 1 - Experimental
Wasm modules
Stability: 1 - Experimental
顶层 await
await
可以用在 ES 模块的顶层代码中:
// `a.mjs`
export const five = await Promise.resolve(5);
// `b.mjs`
import { five } from './a.mjs';
console.log(five); // => `5`
如果顶层的 await
表达式没有得到 resolve ,Node 进程会以 13 状态码退出。
HTTPS 及 HTTP 导入
Stability: 1 - Experimental
Loaders
The former Loaders documentation is now at Modules: Customization hooks.
路径解析及加载算法
功能特性
The default resolver has the following properties:
- FileURL-based resolution as is used by ES modules
- Relative and absolute URL resolution
- No default extensions
- No folder mains
- Bare specifier package resolution lookup through node_modules
- Does not fail on unknown extensions or protocols
- Can optionally provide a hint of the format to the loading phase
The default loader has the following properties
- Support for builtin module loading via
node:
URLs - Support for "inline" module loading via
data:
URLs - Support for
file:
module loading - Fails on any other URL protocol
- Fails on unknown extensions for
file:
loading (supports only.cjs
,.js
, and.mjs
)
解析算法
The algorithm to load an ES module specifier is given through the ESM_RESOLVE method below. It returns the resolved URL for a module specifier relative to a parentURL.
The resolution algorithm determines the full resolved URL for a module
load, along with its suggested module format. The resolution algorithm
does not determine whether the resolved URL protocol can be loaded,
or whether the file extensions are permitted, instead these validations
are applied by Node.js during the load phase
(for example, if it was asked to load a URL that has a protocol that is
not file:
, data:
, node:
, or if --experimental-network-imports
is enabled, https:
).
The algorithm also tries to determine the format of the file based
on the extension (see ESM_FILE_FORMAT
algorithm below). If it does
not recognize the file extension (eg if it is not .mjs
, .cjs
, or
.json
), then a format of undefined
is returned,
which will throw during the load phase.
The algorithm to determine the module format of a resolved URL is provided by ESM_FILE_FORMAT, which returns the unique module format for any file. The "module" format is returned for an ECMAScript Module, while the "commonjs" format is used to indicate loading through the legacy CommonJS loader. Additional formats such as "addon" can be extended in future updates.
In the following algorithms, all subroutine errors are propagated as errors of these top-level routines unless stated otherwise.
defaultConditions is the conditional environment name array,
["node", "import"]
.
The resolver can throw the following errors:
- Invalid Module Specifier: Module specifier is an invalid URL, package name or package subpath specifier.
- Invalid Package Configuration: package.json configuration is invalid or contains an invalid configuration.
- Invalid Package Target: Package exports or imports define a target module for the package that is an invalid type or string target.
- Package Path Not Exported: Package exports do not define or permit a target subpath in the package for the given module.
- Package Import Not Defined: Package imports do not define the specifier.
- Module Not Found: The package or module requested does not exist.
- Unsupported Directory Import: The resolved path corresponds to a directory, which is not a supported target for module imports.
Resolution Algorithm Specification
ESM_RESOLVE(specifier, parentURL)
- Let resolved be undefined.
- If specifier is a valid URL, then
- Set resolved to the result of parsing and reserializing specifier as a URL.
- Otherwise, if specifier starts with "/", "./", or "../", then
- Set resolved to the URL resolution of specifier relative to parentURL.
- Otherwise, if specifier starts with "#", then
- Set resolved to the result of PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, defaultConditions).
- Otherwise,
- Note: specifier is now a bare specifier.
- Set resolved the result of PACKAGE_RESOLVE(specifier, parentURL).
- Let format be undefined.
- If resolved is a "file:" URL, then
- If resolved contains any percent encodings of "/" or "\" ("%2F" and "%5C" respectively), then
- Throw an Invalid Module Specifier error.
- If the file at resolved is a directory, then
- Throw an Unsupported Directory Import error.
- If the file at resolved does not exist, then
- Throw a Module Not Found error.
- Set resolved to the real path of resolved, maintaining the same URL querystring and fragment components.
- Set format to the result of ESM_FILE_FORMAT(resolved).
- Otherwise,
- Set format the module format of the content type associated with the URL resolved.
- Return format and resolved to the loading phase
PACKAGE_RESOLVE(packageSpecifier, parentURL)
- Let packageName be undefined.
- If packageSpecifier is an empty string, then
- Throw an Invalid Module Specifier error.
- If packageSpecifier is a Node.js builtin module name, then
- Return the string "node:" concatenated with packageSpecifier.
- If packageSpecifier does not start with "@", then
- Set packageName to the substring of packageSpecifier until the first "/" separator or the end of the string.
- Otherwise,
- If packageSpecifier does not contain a "/" separator, then
- Throw an Invalid Module Specifier error.
- Set packageName to the substring of packageSpecifier until the second "/" separator or the end of the string.
- If packageName starts with "." or contains "\" or "%", then
- Throw an Invalid Module Specifier error.
- Let packageSubpath be "." concatenated with the substring of packageSpecifier from the position at the length of packageName.
- If packageSubpath ends in "/", then
- Throw an Invalid Module Specifier error.
- Let selfUrl be the result of PACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL).
- If selfUrl is not undefined, return selfUrl.
- While parentURL is not the file system root,
- Let packageURL be the URL resolution of "node_modules/" concatenated with packageSpecifier, relative to parentURL.
- Set parentURL to the parent folder URL of parentURL.
- If the folder at packageURL does not exist, then
- Continue the next loop iteration.
- Let pjson be the result of READ_PACKAGE_JSON(packageURL).
- If pjson is not null and pjson.exports is not null or undefined, then
- Return the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
- Otherwise, if packageSubpath is equal to ".", then
- If pjson.main is a string, then
- Return the URL resolution of main in packageURL.
- Otherwise,
- Return the URL resolution of packageSubpath in packageURL.
- Throw a Module Not Found error.
Customizing ESM specifier resolution algorithm
Module customization hooks provide a mechanism for customizing the ESM specifier resolution algorithm. An example that provides CommonJS-style resolution for ESM specifiers is commonjs-extension-resolution-loader.