- assert 断言
- async_hooks 异步钩子
- async_hooks/context 异步上下文
- buffer 缓冲区
- C++插件
- C/C++插件(使用Node-API)
- C++嵌入器
- child_process 子进程
- cluster 集群
- CLI 命令行
- console 控制台
- Corepack 核心包
- crypto 加密
- crypto/webcrypto 网络加密
- debugger 调试器
- deprecation 弃用
- dgram 数据报
- diagnostics_channel 诊断通道
- dns 域名服务器
- domain 域
- Error 错误
- events 事件触发器
- fs 文件系统
- global 全局变量
- http 超文本传输协议
- http2 超文本传输协议2.0
- https 安全超文本传输协议
- inspector 检查器
- Intl 国际化
- module 模块
- module/cjs CommonJS模块
- module/esm ECMAScript模块
- module/package 包模块
- net 网络
- os 操作系统
- path 路径
- perf_hooks 性能钩子
- permission 权限
- process 进程
- punycode 域名代码
- querystring 查询字符串
- readline 逐行读取
- repl 交互式解释器
- report 诊断报告
- sea 单个可执行应用程序
- stream 流
- stream/web 网络流
- string_decoder 字符串解码器
- test 测试
- timers 定时器
- tls 安全传输层
- trace_events 跟踪事件
- tty 终端
- url 网址
- util 实用工具
- v8 引擎
- vm 虚拟机
- wasi 网络汇编系统接口
- worker_threads 工作线程
- zlib 压缩
Node.js v20.2.0 文档
- Node.js v20.2.0
- ► 目录
-
►
导航
- assert 断言
- async_hooks 异步钩子
- async_hooks/context 异步上下文
- buffer 缓冲区
- C++插件
- C/C++插件(使用Node-API)
- C++嵌入器
- child_process 子进程
- cluster 集群
- CLI 命令行
- console 控制台
- Corepack 核心包
- crypto 加密
- crypto/webcrypto 网络加密
- debugger 调试器
- deprecation 弃用
- dgram 数据报
- diagnostics_channel 诊断通道
- dns 域名服务器
- domain 域
- Error 错误
- events 事件触发器
- fs 文件系统
- global 全局变量
- http 超文本传输协议
- http2 超文本传输协议2.0
- https 安全超文本传输协议
- inspector 检查器
- Intl 国际化
- module 模块
- module/cjs CommonJS模块
- module/esm ECMAScript模块
- module/package 包模块
- net 网络
- os 操作系统
- path 路径
- perf_hooks 性能钩子
- permission 权限
- process 进程
- punycode 域名代码
- querystring 查询字符串
- readline 逐行读取
- repl 交互式解释器
- report 诊断报告
- sea 单个可执行应用程序
- stream 流
- stream/web 网络流
- string_decoder 字符串解码器
- test 测试
- timers 定时器
- tls 安全传输层
- trace_events 跟踪事件
- tty 终端
- url 网址
- util 实用工具
- v8 引擎
- vm 虚拟机
- wasi 网络汇编系统接口
- worker_threads 工作线程
- zlib 压缩
- ► 其他版本
- 云服务器
目录
模块:ECMAScript 模块#
介绍#
ECMAScript 模块是 官方标准格式,用于打包 JavaScript 代码以供重用。 模块使用各种 import
和 export
语句定义。
以下是 ES 模块导出函数的示例:
// addTwo.mjs
function addTwo(num) {
return num + 2;
}
export { addTwo };
以下是 ES 模块从 addTwo.mjs
导入函数的示例:
// app.mjs
import { addTwo } from './addTwo.mjs';
// Prints: 6
console.log(addTwo(4));
Node.js 完全支持当前指定的 ECMAScript 模块,并提供它们与其原始模块格式 CommonJS 之间的互操作性。
启用#
Node.js 有两个模块系统: CommonJS 模块和 ECMAScript 模块。
作者可以通过 .mjs
文件扩展名、package.json
"type"
字段、或 --input-type
标志告诉 Node.js 使用 ECMAScript 模块加载器。 在这些情况之外,Node.js 将使用 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"
中定义的路径访问。
有关适用于 Node.js 模块解析中的裸说明符的这些包解析规则的详细信息,请参阅 包文档。
强制文件扩展名#
当使用 import
关键字解析相对或绝对的说明符时,必须提供文件扩展名。 还必须完全指定目录索引(例如 './startup/index.js'
)。
此行为与 import
在浏览器环境中的行为方式相匹配,假设服务器是典型配置的。
URLs#
ES 模块被解析并缓存为 URL。 这意味着特殊字符必须是percent-encoded,例如#
必须是%23
,?
必须是%3F
。
支持 file:
、node:
和 data:
URL 协议。 除非使用 自定义 HTTPS 加载器,否则 Node.js 本身不支持像 'https://example.com/app.js'
这样的说明符。
file:
个网址#
如果用于解析模块的 import
说明符具有不同的查询或片段,则会多次加载模块。
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:
个网址 支持使用以下 MIME 类型导入:
text/javascript
用于 ES 模块application/json
用于 JSONapplication/wasm
用于 Wasm
import 'data:text/javascript,console.log("hello!");';
import _ from 'data:application/json,"world!"' assert { type: 'json' };
data:
URL 仅解析内置模块的 bare specifiers 和 绝对说明符。 解析 相对说明符 不起作用,因为 data:
不是 special scheme。 例如,尝试从 data:text/javascript,import "./foo";
加载 ./foo
无法解析,因为 data:
URL 没有相对解析的概念。
node:
导入#
node:
支持 URL 作为加载 Node.js 内置模块的替代方法。 此 URL 协议允许有效的绝对的 URL 字符串引用内置模块。
import fs from 'node:fs/promises';
导入断言#
导入断言提案 为模块导入语句添加了内联语法,以便在模块说明符旁边传递更多信息。
import fooData from './foo.json' assert { type: 'json' };
const { default: barData } =
await import('./bar.json', { assert: { type: 'json' } });
Node.js 支持以下 type
值,其断言是强制性的:
断言 type | 需要的 |
---|---|
'json' | JSON 模块 |
内置模块#
核心模块 提供其公共 API 的命名导出。 还提供了默认导出,其是 CommonJS 导出的值。
默认导出可用于修改命名导出等。 内置模块的命名导出仅通过调用 module.syncBuiltinESMExports()
进行更新。
import EventEmitter from 'node:events';
const e = new EventEmitter();
import { readFile } from 'node:fs';
readFile('./foo.txt', (err, source) => {
if (err) {
console.error(err);
} else {
console.log(source);
}
});
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 和 ES 模块都支持 动态 import()
。 在 CommonJS 模块中它可以用来加载 ES 模块。
import.meta
#
import.meta
元属性是包含以下属性的 Object
。
import.meta.url
#
- <string> 模块的绝对的
file:
URL。
这与提供当前模块文件 URL 的浏览器中的定义完全相同。
这可以启用有用的模式,例如相对文件加载
import { readFileSync } from 'node:fs';
const buffer = readFileSync(new URL('./data.proto', import.meta.url));
import.meta.resolve(specifier[, parent])
#
此特性仅在启用 --experimental-import-meta-resolve
命令标志时可用。
specifier
<string> 相对于parent
解析的模块说明符。parent
<string> | <URL> 要解析的绝对的父模块 URL。 如果未指定,则使用import.meta.url
的值作为默认值。- 返回: <string>
提供作用域为每个模块的模块相关解析函数,返回 URL 字符串。 与浏览器行为一致,这现在同步返回。
警告 这可能会导致同步文件系统操作,这会像
require.resolve
一样影响性能。
const dependencyAsset = import.meta.resolve('component-lib/asset.css');
import.meta.resolve
还接受第二个参数,它是要从中解析的父模块:
import.meta.resolve('./dep', import.meta.url);
与 CommonJS 的互操作性#
import
语句#
import
语句可以引用 ES 模块或 CommonJS 模块。
import
语句只允许在 ES 模块中使用,但 CommonJS 支持动态 import()
表达式来加载 ES 模块。
导入 CommonJS 模块 时,module.exports
对象作为默认导出提供。 命名导出可能可用,由静态分析提供,以方便更好的生态系统兼容性。
require
#
CommonJS 模块 require
总是将它引用的文件视为 CommonJS。
不支持使用 require
加载 ES 模块,因为 ES 模块具有异步执行。 而是,使用 import()
从 CommonJS 模块加载 ES 模块。
CommonJS 命名空间#
CommonJS 模块由可以是任何类型的 module.exports
对象组成。
当导入 CommonJS 模块时,可以使用 ES 模块默认导入或其对应的语法糖可靠地导入:
import { default as cjs } from 'cjs';
// The following import statement is "syntax sugar" (equivalent but sweeter)
// for `{ default as cjsSugar }` in the above import statement:
import cjsSugar from 'cjs';
console.log(cjs);
console.log(cjs === cjsSugar);
// Prints:
// <module.exports>
// true
CommonJS 模块的 ECMAScript 模块命名空间表示始终是使用 default
导出键指向 CommonJS module.exports
值的命名空间。
当使用 import * as m from 'cjs'
或动态导入时,可以直接观察到此模块命名空间外来对象:
import * as m from 'cjs';
console.log(m);
console.log(m === await import('cjs'));
// Prints:
// [Module] { default: <module.exports> }
// true
为了更好地兼容 JS 生态系统中的现有用法,Node.js 还尝试确定每个导入的 CommonJS 模块的 CommonJS 命名导出,以使用静态分析过程将它们作为单独的 ES 模块导出提供。
例如,考虑编写的 CommonJS 模块:
// cjs.cjs
exports.name = 'exported';
前面的模块支持 ES 模块中的命名导入:
import { name } from './cjs.cjs';
console.log(name);
// Prints: 'exported'
import cjs from './cjs.cjs';
console.log(cjs);
// Prints: { name: 'exported' }
import * as m from './cjs.cjs';
console.log(m);
// Prints: [Module] { default: { name: 'exported' }, name: 'exported' }
从上一个记录模块命名空间外来对象的示例中可以看出,name
导出是从 module.exports
对象复制出来的,并在导入模块时直接设置在 ES 模块命名空间上。
未检测到这些命名导出的实时绑定更新或添加到 module.exports
的新导出。
命名导出的检测基于通用语法模式,但并不总是正确地检测命名导出。 在这些情况下,使用上述默认导入形式可能是更好的选择。
命名导出检测涵盖了许多常见的导出模式、再导出模式、以及构建工具和转译器输出。 有关实现的确切语义,请参阅 cjs-module-lexer。
ES 模块和 CommonJS 的区别#
无 require
、exports
或 module.exports
#
在大多数情况下,可以使用 ES 模块 import
加载 CommonJS 模块。
如果需要,可以使用 module.createRequire()
在 ES 模块中构造 require
函数。
无 __filename
或 __dirname
#
这些 CommonJS 变量在 ES 模块中不可用。
__filename
和 __dirname
用例可以通过 import.meta.url
复制。
没有插件加载#
插件 当前不支持 ES 模块导入。
它们可以改为加载 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
#
require.extensions
没有被 import
使用。 期望加载器钩子在未来可以提供这个工作流。
没有 require.cache
#
require.cache
没有被 import
使用,因为 ES 模块加载器有自己独立的缓存。
JSON 模块#
import
可以引用 JSON 文件:
import packageConfig from './package.json' assert { type: 'json' };
assert { type: 'json' }
语法是强制性的; 见 导入断言。
导入的 JSON 只暴露一个 default
导出。 不支持命名导出。 在 CommonJS 缓存中创建缓存条目,以避免重复。
如果 JSON 模块已经从同一路径导入,则在 CommonJS 中返回相同的对象。
Wasm 模块#
在 --experimental-wasm-modules
标志下支持导入 WebAssembly 模块,允许将任何 .wasm
文件作为普通模块导入,同时也支持它们的模块导入。
这种整合是符合WebAssembly 的 ES 模块集成提案的。
例如,index.mjs
包含:
import * as M from './module.wasm';
console.log(M);
在以下条件下执行:
node --experimental-wasm-modules index.mjs
将为 module.wasm
的实例化提供导出接口。
顶层 await
#
await
关键字可以用在 ECMAScript 模块的顶层主体中。
假设 a.mjs
具有
export const five = await Promise.resolve(5);
并且 b.mjs
具有
import { five } from './a.mjs';
console.log(five); // Logs `5`
node b.mjs # works
如果顶层 await
表达式永远无法解析,则 node
进程将退出并返回 13
状态码。
import { spawn } from 'node:child_process';
import { execPath } from 'node:process';
spawn(execPath, [
'--input-type=module',
'--eval',
// Never-resolving Promise:
'await new Promise(() => {})',
]).once('exit', (code) => {
console.log(code); // Logs `13`
});
HTTPS 和 HTTP 导入#
在 --experimental-network-imports
标志下支持使用 https:
和 http:
导入基于网络的模块。 这允许类似网络浏览器的导入在 Node.js 中工作,但由于应用程序稳定性和安全问题在特权环境而不是浏览器沙箱中运行时会有所不同,因此存在一些差异。
导入仅限于 HTTP/1#
尚不支持 HTTP/2 和 HTTP/3 的自动协议协商。
HTTP 仅限于环回地址#
http:
易受中间人攻击,不允许用于 IPv4 地址 127.0.0.0/8
(127.0.0.1
到 127.255.255.255
)和 IPv6 地址 ::1
之外的地址。 对 http:
的支持旨在用于本地开发。
身份验证永远不会发送到目标服务器。#
Authorization
、Cookie
和 Proxy-Authorization
标头未发送到服务器。 避免在部分导入的 URL 中包含用户信息。 正在研究在服务器上安全使用这些的安全模型。
永远不会在目标服务器上检查 CORS#
CORS 旨在允许服务器将 API 的使用者限制为一组特定的主机。 这不受支持,因为它对于基于服务器的实现没有意义。
无法加载非网络依赖#
这些模块不能访问不超过 http:
或 https:
的其他模块。
要在避免安全问题的同时仍然访问本地模块,则传入对本地依赖项的引用:
// file.mjs
import worker_threads from 'node:worker_threads';
import { configure, resize } from 'https://example.com/imagelib.mjs';
configure({ worker_threads });
// https://example.com/imagelib.mjs
let worker_threads;
export function configure(opts) {
worker_threads = opts.worker_threads;
}
export function resize(img, size) {
// Perform resizing in worker_thread to avoid main thread blocking
}
默认情况下不启用基于网络的加载#
目前,需要 --experimental-network-imports
标志来启用通过 http:
或 https:
加载资源。 将来,将使用不同的机制来执行此操作。 需要选择加入以防止不经意间使用可能影响 Node.js 应用程序可靠性的潜在可变状态的传递依赖关系。
加载器#
此 API 目前正在重新设计,并且仍会发生变化。
要自定义默认的模块解析,则可以选择通过 Node.js 的 --experimental-loader ./loader-name.mjs
参数提供加载器钩子。
当使用钩子时,它们适用于每个后续加载器、入口点和所有 import
调用。 它们不适用于 require
调用; 那些仍然遵循 CommonJS 规则。
加载器遵循 --require
的模式:
node \
--experimental-loader unpkg \
--experimental-loader http-to-https \
--experimental-loader cache-buster
它们按以下顺序调用: cache-buster
调用 http-to-https
,http-to-https
调用 unpkg
。
钩子#
钩子是链的一部分,即使该链仅由一个自定义(用户提供的)钩子和始终存在的默认钩子组成。 钩子函数嵌套: 每个都必须始终返回一个普通对象,并且链接是由于每个函数调用 next<hookName>()
而发生的,next<hookName>()
是对后续加载程序钩子的引用。
返回缺少必需属性的值的钩子会触发异常。
没有调用 next<hookName>()
和没有返回 shortCircuit: true
就返回的钩子也会触发异常。 这些错误有助于防止链中的意外中断。
钩子在一个单独的线程中运行,与主线程隔离。 这意味着它是一个不同的realm。 hooks线程随时可能被主线程终止,所以不要依赖异步操作(如console.log
)来完成。
resolve(specifier, context, nextResolve)
#
加载程序 API 正在重新设计。 这个钩子可能会消失或者它的签名可能会改变。 不要依赖下面描述的 API。
specifier
<string>context
<Object>conditions
<string[]> 相关package.json
的导出条件importAssertions
<Object> 一个对象,其键值对表示要导入的模块的断言parentURL
<string> | <undefined> 导入此模块的模块,如果这是 Node.js 入口点,则为未定义
nextResolve
<Function> 链中后续的resolve
钩子,或者用户提供的最后一个resolve
钩子之后的 Node.js 默认resolve
钩子- 返回: <Object> | <Promise>
format
<string> | <null> | <undefined> 加载钩子的提示(可能会被忽略)'builtin' | 'commonjs' | 'json' | 'module' | 'wasm'
importAssertions
<Object> | <undefined> 缓存模块时要使用的导入断言(可选;如果排除,将使用输入)shortCircuit
<undefined> | <boolean> 此钩子打算终止resolve
钩子链的信号。 默认值:false
url
<string> 此输入解析到的绝对 URL
警告 尽管支持返回 promise 和异步函数,但调用
resolve
可能会阻塞主线程,从而影响性能。
resolve
钩子链负责告诉 Node.js 在哪里查找以及如何缓存给定的 import
语句或表达式。 它可以选择返回其格式(例如 'module'
)作为对 load
钩子的提示。 如果指定了格式,load
钩子最终负责提供最终的 format
值(并且可以随意忽略 resolve
提供的提示); 如果 resolve
提供 format
,则需要自定义 load
钩子,即使只是将值传递给 Node.js 默认的 load
钩子。
导入类型断言是缓存键的一部分,用于将加载的模块保存到内部模块缓存中。 如果模块应该使用与源代码中存在的断言不同的断言进行缓存,则 resolve
钩子负责返回 importAssertions
对象。
context
中的 conditions
属性是适用于此解析请求的 包导出条件 的条件数组。 它们可用于在别处查找条件映射或在调用默认解析逻辑时修改列表。
当前的 包导出条件 总是在传入钩子的 context.conditions
数组中。 为了保证在调用 defaultResolve
时默认的 Node.js 模块说明符解析行为,传递给它的 context.conditions
数组必须包括最初传递到 resolve
钩子的 context.conditions
数组的所有元素。
export function resolve(specifier, context, nextResolve) {
const { parentURL = null } = context;
if (Math.random() > 0.5) { // Some condition.
// For some or all specifiers, do some custom logic for resolving.
// Always return an object of the form {url: <string>}.
return {
shortCircuit: true,
url: parentURL ?
new URL(specifier, parentURL).href :
new URL(specifier).href,
};
}
if (Math.random() < 0.5) { // Another condition.
// When calling `defaultResolve`, the arguments can be modified. In this
// case it's adding another value for matching conditional exports.
return nextResolve(specifier, {
...context,
conditions: [...context.conditions, 'another-condition'],
});
}
// Defer to the next hook in the chain, which would be the
// Node.js default resolve if this is the last user-specified loader.
return nextResolve(specifier);
}
load(url, context, nextLoad)
#
加载程序 API 正在重新设计。 这个钩子可能会消失或者它的签名可能会改变。 不要依赖下面描述的 API。
在此 API 的先前版本中,它分为 3 个独立的、现已弃用的钩子(
getFormat
、getSource
和transformSource
)。
url
<string>resolve
链返回的 URLcontext
<Object>conditions
<string[]> 相关package.json
的导出条件format
<string> | <null> | <undefined>resolve
钩子链可选提供的格式importAssertions
<Object>
nextLoad
<Function> 链中后续的load
钩子,或者用户提供的最后一个load
钩子之后的 Node.js 默认load
钩子- 返回: <Object>
format
<string>shortCircuit
<undefined> | <boolean> 此钩子打算终止resolve
钩子链的信号。 默认值:false
source
<string> | <ArrayBuffer> | <TypedArray> Node.js 评估的来源
load
钩子提供了一种方式来定义确定网址应如何解释、检索、以及解析的自定义方法。 它还负责验证导入断言。
format
的最终值必须是以下之一:
format | 描述 | load 返回的 source 的可接受类型 |
---|---|---|
'builtin' | 加载 Node.js 内置模块 | 不适用 |
'commonjs' | 加载 Node.js CommonJS 模块 | 不适用 |
'json' | 加载一个 JSON 文件 | { string , ArrayBuffer , TypedArray } |
'module' | 加载一个 ES 模块 | { string , ArrayBuffer , TypedArray } |
'wasm' | 加载 WebAssembly 模块 | { ArrayBuffer , TypedArray } |
source
的值对于类型 'builtin'
被忽略,因为目前无法替换 Node.js 内置(核心)模块的值。 对于类型 'commonjs'
,source
的值被忽略,因为 CommonJS 模块加载器没有为 ES 模块加载器提供覆盖 CommonJS 模块返回值 的机制。 这个限制将来可能会被克服。
警告: ESM
load
钩子和来自 CommonJS 模块的命名空间导出不兼容。 尝试将它们一起使用将导致导入的对象为空。 这可能会在未来得到解决。
这些类型都对应于 ECMAScript 中定义的类。
- 特定的
ArrayBuffer
对象是SharedArrayBuffer
。 - 特定的
TypedArray
对象是Uint8Array
。
如果基于文本的格式(即 'json'
、'module'
)的源值不是字符串,则使用 util.TextDecoder
将其转换为字符串。
load
钩子提供了一种方法来定义用于检索 ES 模块说明符的源代码的自定义方法。 这将允许加载器潜在地避免从磁盘读取文件。 它还可以用于将无法识别的格式映射到支持的格式,例如 yaml
到 module
。
export async function load(url, context, nextLoad) {
const { format } = context;
if (Math.random() > 0.5) { // Some condition
/*
For some or all URLs, do some custom logic for retrieving the source.
Always return an object of the form {
format: <string>,
source: <string|buffer>,
}.
*/
return {
format,
shortCircuit: true,
source: '...',
};
}
// Defer to the next hook in the chain.
return nextLoad(url);
}
在更高级的场景中,这也可用于将不受支持的来源转换为受支持的来源(请参阅下面的 示例)。
globalPreload()
#
加载程序 API 正在重新设计。 这个钩子可能会消失或者它的签名可能会改变。 不要依赖下面描述的 API。
在此 API 的先前版本中,此钩子名为
getGlobalPreloadCode
。
context
<Object> 辅助预加载代码的信息port
<MessagePort>
- 返回: <string> 应用程序启动前运行的代码
有时可能需要在应用程序运行所在的同一全局范围内运行一些代码。 此钩子允许返回在启动时作为宽松模式脚本运行的字符串。
类似于 CommonJS 封装器的工作方式,代码在隐式函数范围内运行。 唯一的参数是一个类似 require
的函数,可用于加载像 "fs" 这样的内置函数: getBuiltin(request: string)
.
如果代码需要更高级的 require
特性,则必须使用 module.createRequire()
构建自己的 require
。
export function globalPreload(context) {
return `\
globalThis.someInjectedProperty = 42;
console.log('I just set some globals!');
const { createRequire } = getBuiltin('module');
const { cwd } = getBuiltin('process');
const require = createRequire(cwd() + '/<preload>');
// [...]
`;
}
为了允许应用程序和加载器之间的通信,向预加载代码提供了另一个参数: port
. 这可以作为加载器钩子的参数和钩子返回的源文本内部。
必须注意正确调用 port.ref()
和 port.unref()
以防止进程处于无法正常关闭的状态。
/**
* This example has the application context send a message to the loader
* and sends the message back to the application context
*/
export function globalPreload({ port }) {
port.onmessage = (evt) => {
port.postMessage(evt.data);
};
return `\
port.postMessage('console.log("I went to the Loader and back");');
port.onmessage = (evt) => {
eval(evt.data);
};
`;
}
示例#
各种加载器钩子可以一起使用来完成对 Node.js 代码加载和评估行为的广泛定制。
HTTPS 加载器#
在当前的 Node.js 中,以 https://
开头的说明符是实验性的(请参阅 HTTPS 和 HTTP 导入)。
下面的加载器注册钩子以启用对此类说明符的基本支持。 虽然这看起来像是对 Node.js 核心功能的重大改进,但实际使用此加载器有很大的缺点: 性能比从磁盘加载文件慢得多,没有缓存,也没有安全性。
// https-loader.mjs
import { get } from 'node:https';
export function resolve(specifier, context, nextResolve) {
const { parentURL = null } = context;
// Normally Node.js would error on specifiers starting with 'https://', so
// this hook intercepts them and converts them into absolute URLs to be
// passed along to the later hooks below.
if (specifier.startsWith('https://')) {
return {
shortCircuit: true,
url: specifier,
};
} else if (parentURL && parentURL.startsWith('https://')) {
return {
shortCircuit: true,
url: new URL(specifier, parentURL).href,
};
}
// Let Node.js handle all other specifiers.
return nextResolve(specifier);
}
export function load(url, context, nextLoad) {
// For JavaScript to be loaded over the network, we need to fetch and
// return it.
if (url.startsWith('https://')) {
return new Promise((resolve, reject) => {
get(url, (res) => {
let data = '';
res.setEncoding('utf8');
res.on('data', (chunk) => data += chunk);
res.on('end', () => resolve({
// This example assumes all network-provided JavaScript is ES module
// code.
format: 'module',
shortCircuit: true,
source: data,
}));
}).on('error', (err) => reject(err));
});
}
// Let Node.js handle all other URLs.
return nextLoad(url);
}
// main.mjs
import { VERSION } from 'https://coffeescript.org/browser-compiler-modern/coffeescript.js';
console.log(VERSION);
使用前面的加载器,运行 node --experimental-loader ./https-loader.mjs ./main.mjs
会在 main.mjs
中的 URL 处按照模块打印当前版本的 CoffeeScript。
转译加载器#
可以使用 load
钩子 将 Node.js 无法理解的格式的源代码转换为 JavaScript。 然而,在调用该钩子之前,resolve
钩子 需要告诉 Node.js 不要对未知文件类型抛出错误。
这比在运行 Node.js 之前转换源文件的性能要差; 转译器加载器应该只用于开发和测试目的。
// coffeescript-loader.mjs
import { readFile } from 'node:fs/promises';
import { dirname, extname, resolve as resolvePath } from 'node:path';
import { cwd } from 'node:process';
import { fileURLToPath, pathToFileURL } from 'node:url';
import CoffeeScript from 'coffeescript';
const baseURL = pathToFileURL(`${cwd()}/`).href;
// CoffeeScript files end in .coffee, .litcoffee, or .coffee.md.
const extensionsRegex = /\.coffee$|\.litcoffee$|\.coffee\.md$/;
export function resolve(specifier, context, nextResolve) {
if (extensionsRegex.test(specifier)) {
const { parentURL = baseURL } = context;
// Node.js normally errors on unknown file extensions, so return a URL for
// specifiers ending in the CoffeeScript file extensions.
return {
shortCircuit: true,
url: new URL(specifier, parentURL).href,
};
}
// Let Node.js handle all other specifiers.
return nextResolve(specifier);
}
export async function load(url, context, nextLoad) {
if (extensionsRegex.test(url)) {
// Now that we patched resolve to let CoffeeScript URLs through, we need to
// tell Node.js what format such URLs should be interpreted as. Because
// CoffeeScript transpiles into JavaScript, it should be one of the two
// JavaScript formats: 'commonjs' or 'module'.
// CoffeeScript files can be either CommonJS or ES modules, so we want any
// CoffeeScript file to be treated by Node.js the same as a .js file at the
// same location. To determine how Node.js would interpret an arbitrary .js
// file, search up the file system for the nearest parent package.json file
// and read its "type" field.
const format = await getPackageType(url);
// When a hook returns a format of 'commonjs', `source` is ignored.
// To handle CommonJS files, a handler needs to be registered with
// `require.extensions` in order to process the files with the CommonJS
// loader. Avoiding the need for a separate CommonJS handler is a future
// enhancement planned for ES module loaders.
if (format === 'commonjs') {
return {
format,
shortCircuit: true,
};
}
const { source: rawSource } = await nextLoad(url, { ...context, format });
// This hook converts CoffeeScript source code into JavaScript source code
// for all imported CoffeeScript files.
const transformedSource = coffeeCompile(rawSource.toString(), url);
return {
format,
shortCircuit: true,
source: transformedSource,
};
}
// Let Node.js handle all other URLs.
return nextLoad(url);
}
async function getPackageType(url) {
// `url` is only a file path during the first iteration when passed the
// resolved url from the load() hook
// an actual file path from load() will contain a file extension as it's
// required by the spec
// this simple truthy check for whether `url` contains a file extension will
// work for most projects but does not cover some edge-cases (such as
// extensionless files or a url ending in a trailing space)
const isFilePath = !!extname(url);
// If it is a file path, get the directory it's in
const dir = isFilePath ?
dirname(fileURLToPath(url)) :
url;
// Compose a file path to a package.json in the same directory,
// which may or may not exist
const packagePath = resolvePath(dir, 'package.json');
// Try to read the possibly nonexistent package.json
const type = await readFile(packagePath, { encoding: 'utf8' })
.then((filestring) => JSON.parse(filestring).type)
.catch((err) => {
if (err?.code !== 'ENOENT') console.error(err);
});
// Ff package.json existed and contained a `type` field with a value, voila
if (type) return type;
// Otherwise, (if not at the root) continue checking the next directory up
// If at the root, stop and return false
return dir.length > 1 && getPackageType(resolvePath(dir, '..'));
}
# main.coffee
import { scream } from './scream.coffee'
console.log scream 'hello, world'
import { version } from 'node:process'
console.log "Brought to you by Node.js version #{version}"
# scream.coffee
export scream = (str) -> str.toUpperCase()
使用前面的加载器,运行 node --experimental-loader ./coffeescript-loader.mjs main.coffee
会导致 main.coffee
在其源代码从磁盘加载之后但在 Node.js 执行之前被转换为 JavaScript; 对于通过任何加载文件的 import
语句引用的任何 .coffee
、.litcoffee
或 .coffee.md
文件,依此类推。
解析算法#
特性#
解析器具有以下属性:
- ES 模块使用的基于 FileURL 的解析
- 支持内置模块加载
- 相对和绝对的网址解析
- 没有默认的扩展名
- 没有主文件夹
- 通过 node_modules 进行裸说明符包解析查找
解析器算法#
加载 ES 模块说明符的算法通过 ESM_RESOLVE 下面的方法。 它返回相对于 parentURL 的模块说明符的解析 URL。
确定解析 URL 的模块格式的算法由 ESM_FORMAT 提供,它返回任何文件的唯一模块格式。 "module" 格式为 ECMAScript 模块返回,而 "commonjs" 格式用于指示通过旧版 CommonJS 加载器加载。 可以在未来的更新中扩展其他格式,例如 "addon"。
在以下算法中,除非另有说明,否则所有子程序错误都将作为这些顶层程序的错误传播。
defaultConditions 是条件环境名称数组,["node", "import"]
。
解析器可能会抛出以下错误:
- 无效的模块说明符_: 模块说明符是无效的 URL、包名称或包子路径说明符。
- 无效的包配置_: package.json 配置无效或包含无效配置。
- 无效的包目标_: 包导出或导入为包定义了一个目标模块,该模块是无效类型或字符串目标。
- 包路径未导出_: 包导出不为给定模块定义或允许包中的目标子路径。
- 包导入未定义_: 包导入不定义说明符。
- 找不到模块_: 请求的包或模块不存在。
- 不支持的目录导入_: 解析的路径对应于一个目录,该目录不是模块导入的受支持目标。
解析器算法规范#
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.
- Load resolved as module format, format.
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.
PACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL)
- Let packageURL be the result of LOOKUP_PACKAGE_SCOPE(parentURL).
- If packageURL is null, then
- Return undefined.
- Let pjson be the result of READ_PACKAGE_JSON(packageURL).
- If pjson is null or if pjson.exports is null or undefined, then
- Return undefined.
- If pjson.name is equal to packageName, then
- Return the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
- Otherwise, return undefined.
PACKAGE_EXPORTS_RESOLVE(packageURL, subpath, exports, conditions)
- If exports is an Object with both a key starting with "." and a key not starting with ".", throw an Invalid Package Configuration error.
- If subpath is equal to ".", then
- Let mainExport be undefined.
- If exports is a String or Array, or an Object containing no keys starting with ".", then
- Set mainExport to exports.
- Otherwise if exports is an Object containing a "." property, then
- Set mainExport to exports["."].
- If mainExport is not undefined, then
- Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, mainExport, null, false, conditions).
- If resolved is not null or undefined, return resolved.
- Otherwise, if exports is an Object and all keys of exports start with ".", then
- Let matchKey be the string "./" concatenated with subpath.
- Let resolved be the result of PACKAGE_IMPORTS_EXPORTS_RESOLVE( matchKey, exports, packageURL, false, conditions).
- If resolved is not null or undefined, return resolved.
- Throw a Package Path Not Exported error.
PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, conditions)
- Assert: specifier begins with "#".
- If specifier is exactly equal to "#" or starts with "#/", then
- Throw an Invalid Module Specifier error.
- Let packageURL be the result of LOOKUP_PACKAGE_SCOPE(parentURL).
- If packageURL is not null, then
- Let pjson be the result of READ_PACKAGE_JSON(packageURL).
- If pjson.imports is a non-null Object, then
- Let resolved be the result of PACKAGE_IMPORTS_EXPORTS_RESOLVE( specifier, pjson.imports, packageURL, true, conditions).
- If resolved is not null or undefined, return resolved.
- Throw a Package Import Not Defined error.
PACKAGE\IMPORTS\EXPORTS\RESOLVE(matchKey, matchObj, packageURL, isImports, conditions_)
- If matchKey is a key of matchObj and does not contain "*", then
- Let target be the value of matchObj[matchKey].
- Return the result of PACKAGE_TARGET_RESOLVE(packageURL, target, null, isImports, conditions).
- Let expansionKeys be the list of keys of matchObj containing only a single "*", sorted by the sorting function PATTERN_KEY_COMPARE which orders in descending order of specificity.
- For each key expansionKey in expansionKeys, do
- Let patternBase be the substring of expansionKey up to but excluding the first "*" character.
- If matchKey starts with but is not equal to patternBase, then
- Let patternTrailer be the substring of expansionKey from the index after the first "*" character.
- If patternTrailer has zero length, or if matchKey ends with patternTrailer and the length of matchKey is greater than or equal to the length of expansionKey, then
- Let target be the value of matchObj[expansionKey].
- Let patternMatch be the substring of matchKey starting at the index of the length of patternBase up to the length of matchKey minus the length of patternTrailer.
- Return the result of PACKAGE_TARGET_RESOLVE(packageURL, target, patternMatch, isImports, conditions).
- Return null.
PATTERN_KEY_COMPARE(keyA, keyB)
- Assert: keyA ends with "/" or contains only a single "*".
- Assert: keyB ends with "/" or contains only a single "*".
- Let baseLengthA be the index of "*" in keyA plus one, if keyA contains "*", or the length of keyA otherwise.
- Let baseLengthB be the index of "*" in keyB plus one, if keyB contains "*", or the length of keyB otherwise.
- If baseLengthA is greater than baseLengthB, return -1.
- If baseLengthB is greater than baseLengthA, return 1.
- If keyA does not contain "*", return 1.
- If keyB does not contain "*", return -1.
- If the length of keyA is greater than the length of keyB, return -1.
- If the length of keyB is greater than the length of keyA, return 1.
- Return 0.
PACKAGE\TARGET\RESOLVE(packageURL, target, patternMatch, isImports, conditions)
- If target is a String, then
- If target does not start with "./", then
- If isImports is false, or if target starts with "../" or "/", or if target is a valid URL, then
- Throw an Invalid Package Target error.
- If patternMatch is a String, then
- Return PACKAGE_RESOLVE(target with every instance of "*" replaced by patternMatch, packageURL + "/").
- Return PACKAGE_RESOLVE(target, packageURL + "/").
- If target split on "/" or "\" contains any "", ".", "..", or "node_modules" segments after the first "." segment, case insensitive and including percent encoded variants, throw an Invalid Package Target error.
- Let resolvedTarget be the URL resolution of the concatenation of packageURL and target.
- Assert: resolvedTarget is contained in packageURL.
- If patternMatch is null, then
- Return resolvedTarget.
- If patternMatch split on "/" or "\" contains any "", ".", "..", or "node_modules" segments, case insensitive and including percent encoded variants, throw an Invalid Module Specifier error.
- Return the URL resolution of resolvedTarget with every instance of "*" replaced with patternMatch.
- Otherwise, if target is a non-null Object, then
- If exports contains any index property keys, as defined in ECMA-262 6.1.7 数组索引, throw an Invalid Package Configuration error.
- For each property p of target, in object insertion order as,
- If p equals "default" or conditions contains an entry for p, then
- Let targetValue be the value of the p property in target.
- Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, targetValue, patternMatch, isImports, conditions).
- If resolved is equal to undefined, continue the loop.
- Return resolved.
- Return undefined.
- Otherwise, if target is an Array, then
- If _target.length is zero, return null.
- For each item targetValue in target, do
- Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, targetValue, patternMatch, isImports, conditions), continuing the loop on any Invalid Package Target error.
- If resolved is undefined, continue the loop.
- Return resolved.
- Return or throw the last fallback resolution null return or error.
- Otherwise, if target is null, return null.
- Otherwise throw an Invalid Package Target error.
ESM_FILE_FORMAT(url)
- Assert: url corresponds to an existing file.
- If url ends in ".mjs", then
- Return "module".
- If url ends in ".cjs", then
- Return "commonjs".
- If url ends in ".json", then
- Return "json".
- Let packageURL be the result of LOOKUP_PACKAGE_SCOPE(url).
- Let pjson be the result of READ_PACKAGE_JSON(packageURL).
- If pjson?.type exists and is "module", then
- If url ends in ".js", then
- Return "module".
- Throw an Unsupported File Extension error.
- Otherwise,
- Throw an Unsupported File Extension error.
LOOKUP_PACKAGE_SCOPE(url)
- Let scopeURL be url.
- While scopeURL is not the file system root,
- Set scopeURL to the parent URL of scopeURL.
- If scopeURL ends in a "node_modules" path segment, return null.
- Let pjsonURL be the resolution of "package.json" within scopeURL.
- if the file at pjsonURL exists, then
- Return scopeURL.
- Return null.
READ_PACKAGE_JSON(packageURL)
- Let pjsonURL be the resolution of "package.json" within packageURL.
- If the file at pjsonURL does not exist, then
- Return null.
- If the file at packageURL does not parse as valid JSON, then
- Throw an Invalid Package Configuration error.
- Return the parsed JSON source of the file at pjsonURL.
自定义 ESM 说明符解析算法#
加载器 API 提供了一种用于自定义 ESM 说明符解析算法的机制。 为 ESM 说明符提供 CommonJS 样式解析的加载器示例是 commonjs-extension-resolution-loader。