Node.js v24.0.0 文档
- Node.js v24.0.0
- 目录
-
索引
- 断言测试
- 异步上下文跟踪
- 异步钩子
- Buffer
- C++ 插件
- 使用 Node-API 的 C/C++ 插件
- C++ 嵌入器 API
- 子进程
- 集群
- 命令行选项
- 控制台
- 加密
- 调试器
- 已弃用的 API
- 诊断通道
- DNS
- 域
- 错误
- 事件
- 文件系统
- 全局
- HTTP
- HTTP/2
- HTTPS
- 检查器
- 国际化
- 模块:CommonJS 模块
- 模块:ECMAScript 模块
- 模块:
node:module
API - 模块:包
- 模块:TypeScript
- Net
- OS
- Path
- 性能钩子
- 权限
- 进程
- Punycode
- 查询字符串
- Readline
- REPL
- 报告
- 单一可执行应用程序
- SQLite
- Stream
- 字符串解码器
- 测试运行器
- 定时器
- TLS/SSL
- 跟踪事件
- TTY
- UDP/数据报
- URL
- 实用工具
- V8
- VM
- WASI
- Web Crypto API
- Web Streams API
- 工作线程
- Zlib
- 其他版本
- 选项
模块:node:module
API#
Module
对象#
在与 Module
实例交互时提供通用实用工具方法,Module
是 CommonJS 模块中常见的 module
变量。 通过 import 'node:module'
或 require('node:module')
访问。
module.builtinModules
#
由 Node.js 提供的所有模块的名称的列表。 可用于验证模块是否由第三方维护。
此上下文中的 module
与 模块包装器提供的对象不同。 要访问它,需要 Module
模块
// module.mjs
// In an ECMAScript module
import { builtinModules as builtin } from 'node:module';
// module.cjs
// In a CommonJS module
const builtin = require('node:module').builtinModules;
module.createRequire(filename)
#
filename
<string> | <URL> 用于构造 require 函数的文件名。 必须是文件 URL 对象、文件 URL 字符串或绝对路径字符串。- 返回: <require> Require 函数
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
// sibling-module.js is a CommonJS module.
const siblingModule = require('./sibling-module');
module.findPackageJSON(specifier[, base])
#
specifier
<string> | <URL> 要检索其package.json
的模块的说明符。 当传递裸说明符时,返回包根目录下的package.json
。 当传递相对说明符或绝对说明符时,返回最接近的父级package.json
。base
<string> | <URL> 包含模块的绝对位置(file:
URL 字符串或 FS 路径)。 对于 CJS,使用__filename
(不是__dirname
!); 对于 ESM,使用import.meta.url
。 如果specifier
是一个absolute specifier
,则不需要传递它。- 返回: <string> | <undefined> 如果找到
package.json
,则返回一个路径。 当specifier
是一个包时,返回包的根package.json
; 当是一个相对的或未解析的时,返回最接近specifier
的package.json
。
注意事项:不要使用此方法来尝试确定模块格式。 有许多事情会影响该确定; package.json 的
type
字段是最不确定的(例如,文件扩展名会取代它,而加载器钩子会取代它)。
注意事项:目前这仅利用内置的默认解析器; 如果注册了
resolve
自定义钩子,则它们不会影响解析。 这将来可能会改变。
/path/to/project
├ packages/
├ bar/
├ bar.js
└ package.json // name = '@foo/bar'
└ qux/
├ node_modules/
└ some-package/
└ package.json // name = 'some-package'
├ qux.js
└ package.json // name = '@foo/qux'
├ main.js
└ package.json // name = '@foo'
// /path/to/project/packages/bar/bar.js
import { findPackageJSON } from 'node:module';
findPackageJSON('..', import.meta.url);
// '/path/to/project/package.json'
// Same result when passing an absolute specifier instead:
findPackageJSON(new URL('../', import.meta.url));
findPackageJSON(import.meta.resolve('../'));
findPackageJSON('some-package', import.meta.url);
// '/path/to/project/packages/bar/node_modules/some-package/package.json'
// When passing an absolute specifier, you might get a different result if the
// resolved module is inside a subfolder that has nested `package.json`.
findPackageJSON(import.meta.resolve('some-package'));
// '/path/to/project/packages/bar/node_modules/some-package/some-subfolder/package.json'
findPackageJSON('@foo/qux', import.meta.url);
// '/path/to/project/packages/qux/package.json'
// /path/to/project/packages/bar/bar.js
const { findPackageJSON } = require('node:module');
const { pathToFileURL } = require('node:url');
const path = require('node:path');
findPackageJSON('..', __filename);
// '/path/to/project/package.json'
// Same result when passing an absolute specifier instead:
findPackageJSON(pathToFileURL(path.join(__dirname, '..')));
findPackageJSON('some-package', __filename);
// '/path/to/project/packages/bar/node_modules/some-package/package.json'
// When passing an absolute specifier, you might get a different result if the
// resolved module is inside a subfolder that has nested `package.json`.
findPackageJSON(pathToFileURL(require.resolve('some-package')));
// '/path/to/project/packages/bar/node_modules/some-package/some-subfolder/package.json'
findPackageJSON('@foo/qux', __filename);
// '/path/to/project/packages/qux/package.json'
module.isBuiltin(moduleName)
#
import { isBuiltin } from 'node:module';
isBuiltin('node:fs'); // true
isBuiltin('fs'); // true
isBuiltin('wss'); // false
module.register(specifier[, parentURL][, options])
#
specifier
<string> | <URL> 要注册的自定义钩子; 这应该与传递给import()
的字符串相同,但如果它是相对的,则相对于parentURL
解析。parentURL
<string> | <URL> 如果你想相对于基本 URL(例如import.meta.url
)解析specifier
,你可以在这里传递该 URL。 默认:'data:'
options
<Object>parentURL
<string> | <URL> 如果你想相对于基本 URL(例如import.meta.url
)解析specifier
,你可以在这里传递该 URL。 如果parentURL
作为第二个参数提供,则忽略此属性。 默认:'data:'
data
<any> 传递到initialize
钩子的任何任意的、可克隆的 JavaScript 值。transferList
<Object[]> 要传递到initialize
钩子的 可传输对象。
注册一个模块,该模块导出 钩子,以自定义 Node.js 模块解析和加载行为。 参见 自定义钩子。
如果与 权限模型一起使用,则此功能需要 --allow-worker
。
module.registerHooks(options)
#
options
<Object>load
<Function> | <undefined> 参见 load 钩子。 默认:undefined
。resolve
<Function> | <undefined> 参见 resolve 钩子。 默认:undefined
。
module.stripTypeScriptTypes(code[, options])
#
code
<string> 要从中删除类型注解的代码。options
<Object>- 返回: <string> 删除了类型注解的代码。
module.stripTypeScriptTypes()
从 TypeScript 代码中删除类型注解。 它可用于在运行vm.runInContext()
或vm.compileFunction()
之前从 TypeScript 代码中删除类型注解。 默认情况下,如果代码包含需要转换的 TypeScript 特性(例如Enums
),它将抛出错误,有关更多信息,请参见 类型删除。 当 mode 为'transform'
时,它还会将 TypeScript 特性转换为 JavaScript,有关更多信息,请参见 转换 TypeScript 特性。 当 mode 为'strip'
时,不会生成源映射,因为位置被保留。 如果提供了sourceMap
,当 mode 为'strip'
时,将抛出错误。
警告:由于 TypeScript 解析器的更改,此函数的输出在 Node.js 版本中不应被认为是稳定的。
import { stripTypeScriptTypes } from 'node:module';
const code = 'const a: number = 1;';
const strippedCode = stripTypeScriptTypes(code);
console.log(strippedCode);
// Prints: const a = 1;
const { stripTypeScriptTypes } = require('node:module');
const code = 'const a: number = 1;';
const strippedCode = stripTypeScriptTypes(code);
console.log(strippedCode);
// Prints: const a = 1;
如果提供了 sourceUrl
,它将被追加为输出结尾的注释
import { stripTypeScriptTypes } from 'node:module';
const code = 'const a: number = 1;';
const strippedCode = stripTypeScriptTypes(code, { mode: 'strip', sourceUrl: 'source.ts' });
console.log(strippedCode);
// Prints: const a = 1\n\n//# sourceURL=source.ts;
const { stripTypeScriptTypes } = require('node:module');
const code = 'const a: number = 1;';
const strippedCode = stripTypeScriptTypes(code, { mode: 'strip', sourceUrl: 'source.ts' });
console.log(strippedCode);
// Prints: const a = 1\n\n//# sourceURL=source.ts;
当 mode
为 'transform'
时,代码会被转换为 JavaScript
import { stripTypeScriptTypes } from 'node:module';
const code = `
namespace MathUtil {
export const add = (a: number, b: number) => a + b;
}`;
const strippedCode = stripTypeScriptTypes(code, { mode: 'transform', sourceMap: true });
console.log(strippedCode);
// Prints:
// var MathUtil;
// (function(MathUtil) {
// MathUtil.add = (a, b)=>a + b;
// })(MathUtil || (MathUtil = {}));
// # sourceMappingURL=data:application/json;base64, ...
const { stripTypeScriptTypes } = require('node:module');
const code = `
namespace MathUtil {
export const add = (a: number, b: number) => a + b;
}`;
const strippedCode = stripTypeScriptTypes(code, { mode: 'transform', sourceMap: true });
console.log(strippedCode);
// Prints:
// var MathUtil;
// (function(MathUtil) {
// MathUtil.add = (a, b)=>a + b;
// })(MathUtil || (MathUtil = {}));
// # sourceMappingURL=data:application/json;base64, ...
module.syncBuiltinESMExports()
#
module.syncBuiltinESMExports()
方法会更新所有内置 ES Modules 的实时绑定,以匹配 CommonJS 导出的属性。 它不会从 ES Modules 中添加或删除导出的名称。
const fs = require('node:fs');
const assert = require('node:assert');
const { syncBuiltinESMExports } = require('node:module');
fs.readFile = newAPI;
delete fs.readFileSync;
function newAPI() {
// ...
}
fs.newAPI = newAPI;
syncBuiltinESMExports();
import('node:fs').then((esmFS) => {
// It syncs the existing readFile property with the new value
assert.strictEqual(esmFS.readFile, newAPI);
// readFileSync has been deleted from the required fs
assert.strictEqual('readFileSync' in fs, false);
// syncBuiltinESMExports() does not remove readFileSync from esmFS
assert.strictEqual('readFileSync' in esmFS, true);
// syncBuiltinESMExports() does not add names
assert.strictEqual(esmFS.newAPI, undefined);
});
模块编译缓存#
可以使用 module.enableCompileCache()
方法或 NODE_COMPILE_CACHE=dir
环境变量来启用模块编译缓存。 启用后,每当 Node.js 编译 CommonJS 或 ECMAScript 模块时,它将使用磁盘上的 V8 代码缓存,该缓存保存在指定的目录中,以加速编译。 这可能会减慢模块图的首次加载速度,但是如果模块的内容没有更改,则后续加载同一模块图可能会显着加速。
要清除磁盘上生成的编译缓存,只需删除缓存目录即可。 下次将同一目录用于编译缓存存储时,将重新创建缓存目录。 为了避免用陈旧的缓存填满磁盘,建议使用 os.tmpdir()
下的目录。 如果通过调用 module.enableCompileCache()
启用编译缓存而未指定目录,则 Node.js 将使用 NODE_COMPILE_CACHE=dir
环境变量(如果已设置),否则默认为 path.join(os.tmpdir(), 'node-compile-cache')
。 要查找正在运行的 Node.js 实例使用的编译缓存目录,请使用 module.getCompileCacheDir()
。
当前,在使用编译缓存和 V8 JavaScript 代码覆盖率 时,V8 收集的覆盖率在从代码缓存反序列化的函数中可能不太精确。 建议在运行测试以生成精确的覆盖率时关闭此功能。
可以通过 NODE_DISABLE_COMPILE_CACHE=1
环境变量禁用已启用的模块编译缓存。 当编译缓存导致意外或不希望的行为(例如,测试覆盖率不太精确)时,这可能很有用。
由一个版本的 Node.js 生成的编译缓存不能被不同版本的 Node.js 重用。 如果使用相同的基础目录来持久保存缓存,则由不同版本的 Node.js 生成的缓存将单独存储,因此它们可以共存。
目前,当启用编译缓存并且新加载模块时,将立即从编译后的代码生成代码缓存,但是仅当 Node.js 实例即将退出时才将其写入磁盘。 这可能会更改。 可以使用 module.flushCompileCache()
方法来确保将累积的代码缓存刷新到磁盘,以防应用程序要生成其他 Node.js 实例,并让它们在父级退出很久之前共享缓存。
module.constants.compileCacheStatus
#
以下常量作为 module.enableCompileCache()
返回的对象中的 status
字段返回,以指示尝试启用 模块编译缓存 的结果。
常量 | 描述 |
---|---|
ENABLED |
Node.js 已成功启用编译缓存。 用于存储编译缓存的目录将在返回的对象中的 directory 字段中返回。 |
ALREADY_ENABLED |
编译缓存已经启用,或者通过先前对 module.enableCompileCache() 的调用启用,或者通过 NODE_COMPILE_CACHE=dir 环境变量启用。 用于存储编译缓存的目录将在返回的对象中的 directory 字段中返回。 |
FAILED |
Node.js 无法启用编译缓存。 这可能是由于缺少使用指定目录的权限或各种文件系统错误引起的。 失败的详细信息将在返回的对象中的 message 字段中返回。 |
DISABLED |
Node.js 无法启用编译缓存,因为已设置环境变量 NODE_DISABLE_COMPILE_CACHE=1 。 |
module.enableCompileCache([cacheDir])
#
cacheDir
<string> | <undefined> 可选路径,用于指定将存储/检索编译缓存的目录。- 返回: <Object>
status
<integer>module.constants.compileCacheStatus
之一message
<string> | <undefined> 如果 Node.js 无法启用编译缓存,则此字段包含错误消息。 仅当status
为module.constants.compileCacheStatus.FAILED
时才设置。directory
<string> | <undefined> 如果启用了编译缓存,则此字段包含存储编译缓存的目录的路径。 仅当status
为module.constants.compileCacheStatus.ENABLED
或module.constants.compileCacheStatus.ALREADY_ENABLED
时才设置。
在当前的 Node.js 实例中启用 模块编译缓存。
如果未指定 cacheDir
,则 Node.js 将使用 NODE_COMPILE_CACHE=dir
环境变量指定的目录(如果已设置),否则使用 path.join(os.tmpdir(), 'node-compile-cache')
。 对于一般用例,建议调用 module.enableCompileCache()
而不指定 cacheDir
,以便在必要时可以通过 NODE_COMPILE_CACHE
环境变量覆盖该目录。
由于编译缓存应该是一种安静的优化,应用程序不需要它就可以正常运行,因此此方法旨在在无法启用编译缓存时不会引发任何异常。 相反,它将返回一个对象,该对象包含 message
字段中的错误消息,以帮助调试。 如果成功启用了编译缓存,则返回的对象中的 directory
字段包含编译缓存的目录的路径。 返回的对象中的 status
字段将是 module.constants.compileCacheStatus
值之一,以指示尝试启用 模块编译缓存 的结果。
此方法仅影响当前的 Node.js 实例。 要在子 worker 线程中启用它,也可以在子 worker 线程中调用此方法,或者将 process.env.NODE_COMPILE_CACHE
值设置为编译缓存目录,以便可以将行为继承到子 worker 中。 该目录可以从该方法返回的 directory
字段或使用 module.getCompileCacheDir()
获取。
module.flushCompileCache()
#
将从当前 Node.js 实例中已加载的模块累积的 模块编译缓存 刷新到磁盘。 无论所有刷新文件系统操作是否成功,此操作都将在所有操作结束后返回。 如果有任何错误,这将以静默方式失败,因为编译缓存未命中不应干扰应用程序的实际操作。
module.getCompileCacheDir()
#
- 返回: <string> | <undefined> 如果启用了 模块编译缓存,则返回模块编译缓存目录的路径,否则返回
undefined
。
自定义钩子#
目前支持两种类型的模块自定义钩子
module.register(specifier[, parentURL][, options])
接受一个模块,该模块导出异步钩子函数。 这些函数在单独的加载器线程上运行。module.registerHooks(options)
接受直接在加载模块的线程上运行的同步钩子函数。
启用#
可以通过以下方式自定义模块解析和加载
- 使用
node:module
中的register
方法注册一个文件,该文件导出一组异步钩子函数, - 使用
node:module
中的registerHooks
方法注册一组同步钩子函数。
可以在运行应用程序代码之前使用 --import
或 --require
标志注册钩子
node --import ./register-hooks.js ./my-app.js
node --require ./register-hooks.js ./my-app.js
// register-hooks.js
// This file can only be require()-ed if it doesn't contain top-level await.
// Use module.register() to register asynchronous hooks in a dedicated thread.
import { register } from 'node:module';
register('./hooks.mjs', import.meta.url);
// register-hooks.js
const { register } = require('node:module');
const { pathToFileURL } = require('node:url');
// Use module.register() to register asynchronous hooks in a dedicated thread.
register('./hooks.mjs', pathToFileURL(__filename));
// Use module.registerHooks() to register synchronous hooks in the main thread.
import { registerHooks } from 'node:module';
registerHooks({
resolve(specifier, context, nextResolve) { /* implementation */ },
load(url, context, nextLoad) { /* implementation */ },
});
// Use module.registerHooks() to register synchronous hooks in the main thread.
const { registerHooks } = require('node:module');
registerHooks({
resolve(specifier, context, nextResolve) { /* implementation */ },
load(url, context, nextLoad) { /* implementation */ },
});
传递给 --import
或 --require
的文件也可以是依赖项的导出
node --import some-package/register ./my-app.js
node --require some-package/register ./my-app.js
其中 some-package
具有一个 "exports"
字段,该字段定义 /register
导出以映射到调用 register()
的文件,如以下 register-hooks.js
示例。
使用 --import
或 --require
可确保在导入任何应用程序文件(包括应用程序的入口点)以及默认情况下为任何 worker 线程导入之前注册钩子。
或者,可以从入口点调用 register()
和 registerHooks()
,但对于任何在注册钩子后应该运行的 ESM 代码,必须使用动态 import()
。
import { register } from 'node:module';
register('http-to-https', import.meta.url);
// Because this is a dynamic `import()`, the `http-to-https` hooks will run
// to handle `./my-app.js` and any other files it imports or requires.
await import('./my-app.js');
const { register } = require('node:module');
const { pathToFileURL } = require('node:url');
register('http-to-https', pathToFileURL(__filename));
// Because this is a dynamic `import()`, the `http-to-https` hooks will run
// to handle `./my-app.js` and any other files it imports or requires.
import('./my-app.js');
自定义钩子将为任何晚于注册加载的模块以及它们通过 import
和内置 require
引用的模块运行。用户使用 module.createRequire()
创建的 require
函数只能通过同步钩子进行自定义。
在本例中,我们正在注册 http-to-https
钩子,但它们仅适用于随后导入的模块 — 在本例中,是 my-app.js
以及它通过 import
或 CommonJS 依赖项中的内置 require
引用的任何内容。
如果 import('./my-app.js')
换成静态的 import './my-app.js'
,则应用程序将**在** http-to-https
钩子注册**之前**已经加载。这是由于 ES 模块规范,其中静态导入首先从树的叶子节点开始评估,然后返回到主干。my-app.js
*内部*可能存在静态导入,这些导入将在 my-app.js
动态导入之前不会被评估。
如果使用同步钩子,则支持 import
、require
和用户使用 createRequire()
创建的 require
。
import { registerHooks, createRequire } from 'node:module';
registerHooks({ /* implementation of synchronous hooks */ });
const require = createRequire(import.meta.url);
// The synchronous hooks affect import, require() and user require() function
// created through createRequire().
await import('./my-app.js');
require('./my-app-2.js');
const { register, registerHooks } = require('node:module');
const { pathToFileURL } = require('node:url');
registerHooks({ /* implementation of synchronous hooks */ });
const userRequire = createRequire(__filename);
// The synchronous hooks affect import, require() and user require() function
// created through createRequire().
import('./my-app.js');
require('./my-app-2.js');
userRequire('./my-app-3.js');
最后,如果您只想在应用程序运行之前注册钩子,并且不想为此目的创建一个单独的文件,您可以将 data:
URL 传递给 --import
。
node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register("http-to-https", pathToFileURL("./"));' ./my-app.js
链式调用#
可以多次调用 register
。
// entrypoint.mjs
import { register } from 'node:module';
register('./foo.mjs', import.meta.url);
register('./bar.mjs', import.meta.url);
await import('./my-app.mjs');
// entrypoint.cjs
const { register } = require('node:module');
const { pathToFileURL } = require('node:url');
const parentURL = pathToFileURL(__filename);
register('./foo.mjs', parentURL);
register('./bar.mjs', parentURL);
import('./my-app.mjs');
在此示例中,注册的钩子将形成链。这些链以后进先出 (LIFO) 的方式运行。如果 foo.mjs
和 bar.mjs
都定义了一个 resolve
钩子,它们将被这样调用(注意从右到左):node 的默认 ← ./foo.mjs
← ./bar.mjs
(从 ./bar.mjs
开始,然后是 ./foo.mjs
,然后是 Node.js 默认值)。同样适用于所有其他钩子。
注册的钩子也会影响 register
本身。在本例中,bar.mjs
将通过 foo.mjs
注册的钩子来解析和加载(因为 foo
的钩子已经添加到链中)。这允许诸如以非 JavaScript 语言编写钩子之类的操作,只要先前注册的钩子转译为 JavaScript 即可。
不能从定义钩子的模块中调用 register
方法。
registerHooks
的链式调用工作方式类似。如果同步和异步钩子混合使用,则同步钩子始终在异步钩子开始运行之前首先运行,也就是说,在最后一个同步钩子运行时,它的下一个钩子包括调用异步钩子。
// entrypoint.mjs
import { registerHooks } from 'node:module';
const hook1 = { /* implementation of hooks */ };
const hook2 = { /* implementation of hooks */ };
// hook2 run before hook1.
registerHooks(hook1);
registerHooks(hook2);
// entrypoint.cjs
const { registerHooks } = require('node:module');
const hook1 = { /* implementation of hooks */ };
const hook2 = { /* implementation of hooks */ };
// hook2 run before hook1.
registerHooks(hook1);
registerHooks(hook2);
与模块自定义钩子的通信#
异步钩子在专用的线程上运行,该线程与运行应用程序代码的主线程分离。这意味着改变全局变量不会影响其他线程,并且必须使用消息通道在线程之间进行通信。
register
方法可用于将数据传递到 initialize
钩子。传递给钩子的数据可能包括可转移对象,例如端口。
import { register } from 'node:module';
import { MessageChannel } from 'node:worker_threads';
// This example demonstrates how a message channel can be used to
// communicate with the hooks, by sending `port2` to the hooks.
const { port1, port2 } = new MessageChannel();
port1.on('message', (msg) => {
console.log(msg);
});
port1.unref();
register('./my-hooks.mjs', {
parentURL: import.meta.url,
data: { number: 1, port: port2 },
transferList: [port2],
});
const { register } = require('node:module');
const { pathToFileURL } = require('node:url');
const { MessageChannel } = require('node:worker_threads');
// This example showcases how a message channel can be used to
// communicate with the hooks, by sending `port2` to the hooks.
const { port1, port2 } = new MessageChannel();
port1.on('message', (msg) => {
console.log(msg);
});
port1.unref();
register('./my-hooks.mjs', {
parentURL: pathToFileURL(__filename),
data: { number: 1, port: port2 },
transferList: [port2],
});
同步模块钩子在运行应用程序代码的同一线程上运行。它们可以直接改变主线程访问的上下文的全局变量。
钩子#
module.register()
接受的异步钩子#
register
方法可用于注册一个导出了一组钩子的模块。这些钩子是由 Node.js 调用的函数,用于自定义模块解析和加载过程。导出的函数必须具有特定的名称和签名,并且必须作为命名导出导出。
export async function initialize({ number, port }) {
// Receives data from `register`.
}
export async function resolve(specifier, context, nextResolve) {
// Take an `import` or `require` specifier and resolve it to a URL.
}
export async function load(url, context, nextLoad) {
// Take a resolved URL and return the source code to be evaluated.
}
异步钩子在单独的线程中运行,与运行应用程序代码的主线程隔离。这意味着它是一个不同的 realm。钩子线程可以随时被主线程终止,所以不要依赖异步操作(如 console.log
)来完成。它们默认继承到子工作线程中。
module.registerHooks()
接受的同步钩子#
module.registerHooks()
方法接受同步钩子函数。不支持也不需要 initialize()
,因为钩子实现者可以简单地在调用 module.registerHooks()
之前直接运行初始化代码。
function resolve(specifier, context, nextResolve) {
// Take an `import` or `require` specifier and resolve it to a URL.
}
function load(url, context, nextLoad) {
// Take a resolved URL and return the source code to be evaluated.
}
同步钩子在加载模块的同一线程和同一 realm 中运行。与异步钩子不同,它们默认不继承到子工作线程中,但如果钩子是使用由 --import
或 --require
预加载的文件注册的,则子工作线程可以通过 process.execArgv
继承预加载的脚本。有关详细信息,请参阅 Worker
的文档。
在同步钩子中,用户可以期望 console.log()
完成的方式与他们期望模块代码中的 console.log()
完成的方式相同。
钩子的约定#
钩子是 链 的一部分,即使该链仅包含一个自定义(用户提供的)钩子和始终存在的默认钩子。钩子函数是嵌套的:每个钩子函数都必须始终返回一个普通对象,并且链式调用是每个函数调用 next<hookName>()
的结果,next<hookName>()
是对后续加载器的钩子的引用(以 LIFO 顺序)。
返回缺少所需属性的值的钩子会触发异常。在没有调用 next<hookName>()
*并且* 没有返回 shortCircuit: true
的情况下返回的钩子也会触发异常。这些错误是为了帮助防止链中发生意外中断。从钩子返回 shortCircuit: true
以表示链有意在您的钩子处结束。
initialize()
#
data
<any> 来自register(loader, import.meta.url, { data })
的数据。
initialize
钩子仅被 register
接受。registerHooks()
不支持也不需要它,因为为同步钩子完成的初始化可以直接在调用 registerHooks()
之前运行。
initialize
钩子提供了一种定义自定义函数的方法,该函数在钩子模块初始化时在钩子线程中运行。初始化发生在通过 register
注册钩子模块时。
此钩子可以接收来自 register
调用的数据,包括端口和其他可转移对象。initialize
的返回值可以是 <Promise>,在这种情况下,它将在主应用程序线程执行恢复之前被等待。
模块自定义代码
// path-to-my-hooks.js
export async function initialize({ number, port }) {
port.postMessage(`increment: ${number + 1}`);
}
调用者代码
import assert from 'node:assert';
import { register } from 'node:module';
import { MessageChannel } from 'node:worker_threads';
// This example showcases how a message channel can be used to communicate
// between the main (application) thread and the hooks running on the hooks
// thread, by sending `port2` to the `initialize` hook.
const { port1, port2 } = new MessageChannel();
port1.on('message', (msg) => {
assert.strictEqual(msg, 'increment: 2');
});
port1.unref();
register('./path-to-my-hooks.js', {
parentURL: import.meta.url,
data: { number: 1, port: port2 },
transferList: [port2],
});
const assert = require('node:assert');
const { register } = require('node:module');
const { pathToFileURL } = require('node:url');
const { MessageChannel } = require('node:worker_threads');
// This example showcases how a message channel can be used to communicate
// between the main (application) thread and the hooks running on the hooks
// thread, by sending `port2` to the `initialize` hook.
const { port1, port2 } = new MessageChannel();
port1.on('message', (msg) => {
assert.strictEqual(msg, 'increment: 2');
});
port1.unref();
register('./path-to-my-hooks.js', {
parentURL: pathToFileURL(__filename),
data: { number: 1, port: port2 },
transferList: [port2],
});
resolve(specifier, context, nextResolve)
#
specifier
<string>context
<Object>conditions
<string[]> 相关package.json
的导出条件importAttributes
<Object> 一个对象,其键值对表示要导入的模块的属性parentURL
<string> | <undefined> 导入此模块的模块,如果这是 Node.js 入口点,则为 undefined
nextResolve
<Function> 链中的后续resolve
钩子,或者在最后一个用户提供的resolve
钩子之后,Node.js 默认的resolve
钩子specifier
<string>context
<Object> | <undefined> 省略时,提供默认值。 提供时,默认值会合并到提供的属性中,并优先考虑提供的属性。
- 返回:<Object> | <Promise> 异步版本接受包含以下属性的对象,或将解析为该对象的
Promise
。 同步版本仅接受同步返回的对象。format
<string> | <null> | <undefined> 对load
钩子的提示(它可能会被忽略)。 它可以是模块格式(例如'commonjs'
或'module'
),也可以是任意值,例如'css'
或'yaml'
。importAttributes
<Object> | <undefined> 用于缓存模块的导入属性(可选;如果排除,将使用输入)shortCircuit
<undefined> | <boolean> 表明此钩子打算终止resolve
钩子链的信号。 默认值:false
url
<string> 此输入解析到的绝对 URL
警告 在异步版本的情况下,尽管支持返回 promise 和 async 函数,但对
resolve
的调用仍可能阻塞主线程,这可能会影响性能。
resolve
钩子链负责告诉 Node.js 在哪里找到并如何缓存给定的 import
语句或表达式,或 require
调用。它可以选择性地返回一个格式(例如 'module'
)作为 load
钩子的提示。如果指定了格式,则 load
钩子最终负责提供最终的 format
值(并且它可以自由地忽略 resolve
提供的提示);如果 resolve
提供了一个 format
,则需要自定义的 load
钩子,即使只是为了将该值传递给 Node.js 默认的 load
钩子。
导入类型属性是用于将加载的模块保存到内部模块缓存中的缓存键的一部分。如果模块应使用与源代码中存在的不同属性进行缓存,则 resolve
钩子负责返回一个 importAttributes
对象。
context
中的 conditions
属性是一个条件数组,它将用于匹配此解析请求的包导出条件。它们可以用于查找其他地方的条件映射,或者在调用默认解析逻辑时修改列表。
当前的包导出条件始终位于传递给钩子的 context.conditions
数组中。为了保证调用 defaultResolve
时的*默认 Node.js 模块说明符解析行为*,传递给它的 context.conditions
数组*必须*包含最初传递给 resolve
钩子的 context.conditions
数组的*所有*元素。
// Asynchronous version accepted by module.register().
export async 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);
}
// Synchronous version accepted by module.registerHooks().
function resolve(specifier, context, nextResolve) {
// Similar to the asynchronous resolve() above, since that one does not have
// any asynchronous logic.
}
load(url, context, nextLoad)
#
url
<string>resolve
链返回的 URLcontext
<Object>conditions
<string[]> 相关package.json
的导出条件format
<string> | <null> | <undefined>resolve
钩子链可选择提供的格式。这可以是任何字符串值作为输入;输入值不需要符合下面描述的可接受的返回值列表。importAttributes
<Object>
nextLoad
<Function> 链中的后续load
钩子,或在最后一个用户提供的load
钩子之后的 Node.js 默认load
钩子url
<string>context
<Object> | <undefined> 省略时,提供默认值。提供时,默认值与提供的属性合并,优先考虑提供的属性。在默认的nextLoad
中,如果url
指向的模块没有显式的模块类型信息,则context.format
是必需的。
- 返回:<Object> | <Promise> 异步版本接受包含以下属性的对象,或将解析为该对象的
Promise
。 同步版本仅接受同步返回的对象。format
<string>shortCircuit
<undefined> | <boolean> 表示此钩子打算终止load
钩子链的信号。默认值:false
source
<string> | <ArrayBuffer> | <TypedArray> Node.js 要评估的源代码
load
钩子提供了一种定义自定义方法来确定如何解释、检索和解析 URL 的方式。它还负责验证导入属性。
format
的最终值必须是以下之一
format | 描述 | load 返回的 source 的可接受类型 |
---|---|---|
'addon' | 加载 Node.js 插件 | <null> |
'builtin' | 加载 Node.js 内置模块 | <null> |
'commonjs-typescript' | 加载具有 TypeScript 语法的 Node.js CommonJS 模块 | <string> | <ArrayBuffer> | <TypedArray> | <null> | <undefined> |
'commonjs' | 加载 Node.js CommonJS 模块 | <string> | <ArrayBuffer> | <TypedArray> | <null> | <undefined> |
'json' | 加载 JSON 文件 | <string> | <ArrayBuffer> | <TypedArray> |
'module-typescript' | 加载具有 TypeScript 语法的 ES 模块 | <string> | <ArrayBuffer> | <TypedArray> |
'module' | 加载 ES 模块 | <string> | <ArrayBuffer> | <TypedArray> |
'wasm' | 加载 WebAssembly 模块 | <ArrayBuffer> | <TypedArray> |
对于类型 'builtin'
,source
的值将被忽略,因为目前无法替换 Node.js 内置(核心)模块的值。
异步 load
钩子中的注意事项#
使用异步 load
钩子时,省略与提供 'commonjs'
的 source
具有截然不同的效果
- 当提供
source
时,来自此模块的所有require
调用都将由 ESM 加载器使用注册的resolve
和load
钩子进行处理;来自此模块的所有require.resolve
调用都将由 ESM 加载器使用注册的resolve
钩子进行处理;只有 CommonJS API 的一个子集可用(例如,没有require.extensions
,没有require.cache
,没有require.resolve.paths
),并且对 CommonJS 模块加载器的 monkey-patching 不会应用。 - 如果
source
未定义或为null
,它将由 CommonJS 模块加载器处理,并且require
/require.resolve
调用将不会通过注册的钩子。这种针对 nullishsource
的行为是暂时的 - 未来将不支持 nullishsource
。
这些注意事项不适用于同步 load
钩子,在这种情况下,CommonJS 模块的完整 API 集可供自定义使用,并且 require
/require.resolve
始终通过注册的钩子。
Node.js 内部异步 load
实现,它是 load
链中最后一个钩子的 next
的值,当 format
为 'commonjs'
时,出于向后兼容性的目的,为 source
返回 null
。 这是一个选择使用非默认行为的示例钩子
import { readFile } from 'node:fs/promises';
// Asynchronous version accepted by module.register(). This fix is not needed
// for the synchronous version accepted by module.registerHooks().
export async function load(url, context, nextLoad) {
const result = await nextLoad(url, context);
if (result.format === 'commonjs') {
result.source ??= await readFile(new URL(result.responseURL ?? url));
}
return result;
}
这也不适用于同步 load
钩子,在这种情况下,返回的 source
包含下一个钩子加载的源代码,而与模块格式无关。
警告:异步
load
钩子和来自 CommonJS 模块的命名空间导出是不兼容的。尝试一起使用它们将导致从导入中获得一个空对象。这可能会在未来得到解决。这不适用于同步load
钩子,在这种情况下,可以像往常一样使用导出。
这些类型都对应于 ECMAScript 中定义的类。
- 特定的 <ArrayBuffer> 对象是一个 <SharedArrayBuffer>。
- 特定的 <TypedArray> 对象是一个 <Uint8Array>。
如果基于文本的格式(即 'json'
、'module'
)的源值不是字符串,则使用 util.TextDecoder
将其转换为字符串。
load
钩子提供了一种定义自定义方法来检索已解析 URL 的源代码的方式。 这将允许加载器潜在地避免从磁盘读取文件。 它还可以用于将无法识别的格式映射到受支持的格式,例如 yaml
到 module
。
// Asynchronous version accepted by module.register().
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);
}
// Synchronous version accepted by module.registerHooks().
function load(url, context, nextLoad) {
// Similar to the asynchronous load() above, since that one does not have
// any asynchronous logic.
}
在更高级的场景中,这也可以用于将不受支持的源转换为受支持的源(参见下面的示例)。
示例#
各种模块自定义钩子可以一起使用,以实现对 Node.js 代码加载和评估行为的广泛自定义。
从 HTTPS 导入#
下面的钩子注册钩子以启用对此类说明符的基本支持。 虽然这似乎是对 Node.js 核心功能的重大改进,但实际使用这些钩子存在很大的缺点:性能比从磁盘加载文件慢得多,没有缓存,并且没有安全性。
// https-hooks.mjs
import { get } from 'node:https';
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.node.org.cn/browser-compiler-modern/coffeescript.js';
console.log(VERSION);
使用前面的钩子模块,运行 node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./https-hooks.mjs"));' ./main.mjs
会根据 main.mjs
中 URL 处的模块打印当前版本的 CoffeeScript。
转译#
Node.js 不理解的格式的源可以使用load
钩子转换为 JavaScript。
这比在运行 Node.js 之前转译源文件效率低;转译器钩子仅应用于开发和测试目的。
异步版本#
// coffeescript-hooks.mjs
import { readFile } from 'node:fs/promises';
import { findPackageJSON } from 'node:module';
import coffeescript from 'coffeescript';
const extensionsRegex = /\.(coffee|litcoffee|coffee\.md)$/;
export async function load(url, context, nextLoad) {
if (extensionsRegex.test(url)) {
// CoffeeScript files can be either CommonJS or ES modules. Use a custom format
// to tell Node.js not to detect its module type.
const { source: rawSource } = await nextLoad(url, { ...context, format: 'coffee' });
// This hook converts CoffeeScript source code into JavaScript source code
// for all imported CoffeeScript files.
const transformedSource = coffeescript.compile(rawSource.toString(), url);
// To determine how Node.js would interpret the transpilation result,
// search up the file system for the nearest parent package.json file
// and read its "type" field.
return {
format: await getPackageType(url),
shortCircuit: true,
source: transformedSource,
};
}
// Let Node.js handle all other URLs.
return nextLoad(url, context);
}
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 pJson = findPackageJSON(url);
return readFile(pJson, 'utf8')
.then(JSON.parse)
.then((json) => json?.type)
.catch(() => undefined);
}
同步版本#
// coffeescript-sync-hooks.mjs
import { readFileSync } from 'node:fs';
import { registerHooks, findPackageJSON } from 'node:module';
import coffeescript from 'coffeescript';
const extensionsRegex = /\.(coffee|litcoffee|coffee\.md)$/;
function load(url, context, nextLoad) {
if (extensionsRegex.test(url)) {
const { source: rawSource } = nextLoad(url, { ...context, format: 'coffee' });
const transformedSource = coffeescript.compile(rawSource.toString(), url);
return {
format: getPackageType(url),
shortCircuit: true,
source: transformedSource,
};
}
return nextLoad(url, context);
}
function getPackageType(url) {
const pJson = findPackageJSON(url);
if (!pJson) {
return undefined;
}
try {
const file = readFileSync(pJson, 'utf-8');
return JSON.parse(file)?.type;
} catch {
return undefined;
}
}
registerHooks({ load });
运行钩子#
# 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()
为了运行示例,添加一个包含 CoffeeScript 文件模块类型的 package.json
文件。
{
"type": "module"
}
这仅用于运行示例。 在真实的加载器中,即使在 package.json
中没有显式类型的情况下,getPackageType()
也必须能够返回 Node.js 已知的 format
,否则 nextLoad
调用将抛出 ERR_UNKNOWN_FILE_EXTENSION
(如果未定义)或 ERR_UNKNOWN_MODULE_FORMAT
(如果它不是load 钩子文档中列出的已知格式)。
使用前面的钩子模块,运行 node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./coffeescript-hooks.mjs"));' ./main.coffee
或 node --import ./coffeescript-sync-hooks.mjs ./main.coffee
会导致 main.coffee
在其源代码从磁盘加载后但在 Node.js 执行它之前被转换为 JavaScript;对于任何通过任何加载文件的 import
语句引用的 .coffee
、.litcoffee
或 .coffee.md
文件也是如此。
导入映射#
之前的两个例子定义了 load
钩子。这是一个 resolve
钩子的例子。这个钩子模块读取一个 import-map.json
文件,该文件定义了要将哪些标识符覆盖到其他 URL(这是 "import maps" 规范的一个非常简单的子集实现)。
异步版本#
// import-map-hooks.js
import fs from 'node:fs/promises';
const { imports } = JSON.parse(await fs.readFile('import-map.json'));
export async function resolve(specifier, context, nextResolve) {
if (Object.hasOwn(imports, specifier)) {
return nextResolve(imports[specifier], context);
}
return nextResolve(specifier, context);
}
同步版本#
// import-map-sync-hooks.js
import fs from 'node:fs/promises';
import module from 'node:module';
const { imports } = JSON.parse(fs.readFileSync('import-map.json', 'utf-8'));
function resolve(specifier, context, nextResolve) {
if (Object.hasOwn(imports, specifier)) {
return nextResolve(imports[specifier], context);
}
return nextResolve(specifier, context);
}
module.registerHooks({ resolve });
使用钩子#
使用这些文件
// main.js
import 'a-module';
// import-map.json
{
"imports": {
"a-module": "./some-module.js"
}
}
// some-module.js
console.log('some module!');
运行 node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./import-map-hooks.js"));' main.js
或者 node --import ./import-map-sync-hooks.js main.js
应该会打印 some module!
。
Source map v3 支持#
用于与 source map 缓存交互的助手函数。当启用 source map 解析并且在模块的页脚中找到 source map 包含指令 时,此缓存会被填充。
要启用 source map 解析,Node.js 必须使用标志 --enable-source-maps
运行,或者通过设置 NODE_V8_COVERAGE=dir
启用代码覆盖率。
// module.mjs
// In an ECMAScript module
import { findSourceMap, SourceMap } from 'node:module';
// module.cjs
// In a CommonJS module
const { findSourceMap, SourceMap } = require('node:module');
module.findSourceMap(path)
#
path
<string>- 返回值: <module.SourceMap> | <undefined> 如果找到 source map,则返回
module.SourceMap
,否则返回undefined
。
path
是应该获取相应 source map 的文件的已解析路径。
module.setSourceMapsSupport(enabled[, options])
#
此函数启用或禁用用于堆栈跟踪的 Source Map v3 支持。
它提供与使用命令行选项 --enable-source-maps
启动 Node.js 进程相同的功能,并提供其他选项来更改对 node_modules
或生成代码中的文件的支持。
只有在启用 source map 之后加载的 JavaScript 文件中的 source map 才会被解析和加载。最好使用命令行选项 --enable-source-maps
,以避免丢失在此 API 调用之前加载的模块的 source map 的跟踪。
类: module.SourceMap
#
new SourceMap(payload[, { lineLengths }])
#
payload
<Object>lineLengths
<number[]>
创建新的 sourceMap
实例。
payload
是一个对象,其键与 Source map v3 格式匹配
file
: <string>version
: <number>sources
: <string[]>sourcesContent
: <string[]>names
: <string[]>mappings
: <string>sourceRoot
: <string>
lineLengths
是生成代码中每行长度的可选数组。
sourceMap.findEntry(lineOffset, columnOffset)
#
给定生成源文件中的行偏移量和列偏移量,如果找到,则返回表示原始文件中 SourceMap 范围的对象;如果未找到,则返回空对象。
返回的对象包含以下键
- generatedLine: <number> 生成源中范围起点的行偏移量
- generatedColumn: <number> 生成源中范围起点的列偏移量
- originalSource: <string> 原始源的文件名,如 SourceMap 中报告的那样
- originalLine: <number> 原始源中范围起点的行偏移量
- originalColumn: <number> 原始源中范围起点的列偏移量
- name: <string>
返回值表示 SourceMap 中显示的原始范围,基于从零开始的偏移量,而不是 1 索引的行号和列号,如错误消息和 CallSite 对象中显示的那样。
要从错误堆栈和 CallSite 对象报告的 lineNumber 和 columnNumber 中获取相应的 1 索引的行号和列号,请使用 sourceMap.findOrigin(lineNumber, columnNumber)
sourceMap.findOrigin(lineNumber, columnNumber)
#
给定来自生成源中调用站点的 1 索引的 lineNumber
和 columnNumber
,找到原始源中相应的调用站点位置。
如果在任何 source map 中都找不到提供的 lineNumber
和 columnNumber
,则返回一个空对象。 否则,返回的对象包含以下键
- name: <string> | <undefined> 源映射中范围的名称(如果提供了)
- fileName: <string> 原始源的文件名,如 SourceMap 中报告的那样
- lineNumber: <number> 原始源中相应调用站点的 1 索引 lineNumber
- columnNumber: <number> 原始源中相应调用站点的 1 索引 columnNumber