模块:node:module
API
新增于:v0.3.7
Module
对象
在与Module
的实例(通常在CommonJS模块中看到的module
变量)交互时,提供通用实用程序方法。通过import 'node:module'
或require('node:module')
访问。
module.builtinModules
[历史]
版本 | 变更 |
---|---|
v23.5.0 | 此列表现在还包含仅前缀模块。 |
v9.3.0, v8.10.0, v6.13.0 | 新增于:v9.3.0, v8.10.0, v6.13.0 |
Node.js 提供的所有模块名称的列表。可用于验证模块是否由第三方维护。
此上下文中的module
与模块包装器提供的对象不同。要访问它,请require
Module
模块:
// module.mjs
// 在 ECMAScript 模块中
import { builtinModules as builtin } from 'node:module'
// module.cjs
// 在 CommonJS 模块中
const builtin = require('node:module').builtinModules
module.createRequire(filename)
新增于: v12.2.0
filename
<string> | <URL> 用于构建require
函数的文件名。必须是文件 URL 对象、文件 URL 字符串或绝对路径字符串。- 返回值: <require>
require
函数
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
// sibling-module.js 是一个 CommonJS 模块。
const siblingModule = require('./sibling-module')
module.findPackageJSON(specifier[, base])
新增于: v23.2.0
specifier
<string> | <URL> 要检索其package.json
的模块的标识符。传递 裸标识符 时,返回包根目录下的package.json
。传递 相对标识符 或 绝对标识符 时,返回最近的父package.json
。base
<string> | <URL> 包含模块的绝对位置(file:
URL 字符串或文件系统路径)。对于 CJS,使用__filename
(不是__dirname
!);对于 ESM,使用import.meta.url
。如果specifier
是一个绝对标识符
,则不需要传递它。- 返回值: <string> | <undefined> 如果找到
package.json
,则返回路径。当startLocation
是一个包时,返回包的根package.json
;当是相对的或未解析的时,返回startLocation
最近的package.json
。
/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'
// 传递绝对标识符也能得到相同的结果:
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'
// 传递绝对标识符时,如果解析的模块位于具有嵌套 `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'
// 传递绝对标识符也能得到相同的结果:
findPackageJSON(pathToFileURL(path.join(__dirname, '..')))
findPackageJSON('some-package', __filename)
// '/path/to/project/packages/bar/node_modules/some-package/package.json'
// 传递绝对标识符时,如果解析的模块位于具有嵌套 `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)
新增于:v18.6.0, v16.17.0
import { isBuiltin } from 'node:module'
isBuiltin('node:fs') // true
isBuiltin('fs') // true
isBuiltin('wss') // false
module.register(specifier[, parentURL][, options])
[历史]
版本 | 变更 |
---|---|
v20.8.0, v18.19.0 | 添加对 WHATWG URL 实例的支持。 |
v20.6.0, v18.19.0 | 新增于:v20.6.0, v18.19.0 |
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 模块解析和加载行为。请参阅 自定义挂钩。
module.registerHooks(options)
新增于: v23.5.0
注册自定义 Node.js 模块解析和加载行为的钩子。参见 自定义钩子。
module.stripTypeScriptTypes(code[, options])
新增于:v23.2.0
code
<字符串> 要从中去除类型注解的代码。options
<对象>返回值:<字符串> 去除类型注解后的代码。
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)
// 输出:const a = 1;
const { stripTypeScriptTypes } = require('node:module')
const code = 'const a: number = 1;'
const strippedCode = stripTypeScriptTypes(code)
console.log(strippedCode)
// 输出: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)
// 输出: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)
// 输出: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)
// 输出:
// 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)
// 输出:
// var MathUtil;
// (function(MathUtil) {
// MathUtil.add = (a, b)=>a + b;
// })(MathUtil || (MathUtil = {}));
// # sourceMappingURL=data:application/json;base64, ...
module.syncBuiltinESMExports()
Added in: v12.12.0
module.syncBuiltinESMExports()
方法更新所有内置 ES 模块 的实时绑定,使其与 CommonJS 模块的导出属性匹配。它不会从 ES 模块 中添加或删除导出的名称。
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 => {
// 它将现有的 readFile 属性与新值同步
assert.strictEqual(esmFS.readFile, newAPI)
// readFileSync 已从已加载的 fs 中删除
assert.strictEqual('readFileSync' in fs, false)
// syncBuiltinESMExports() 不会从 esmFS 中删除 readFileSync
assert.strictEqual('readFileSync' in esmFS, true)
// syncBuiltinESMExports() 不会添加名称
assert.strictEqual(esmFS.newAPI, undefined)
})
模块编译缓存
[历史]
版本 | 变更 |
---|---|
v22.8.0 | 添加了用于运行时访问的初始 JavaScript API。 |
v22.1.0 | 在 v22.1.0 中添加 |
模块编译缓存可以通过使用 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
新增于:v22.8.0
以下常量作为 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])
新增于: v22.8.0
cacheDir
<字符串> | <未定义> 可选路径,用于指定编译缓存的存储/读取目录。- 返回值: <对象>
status
<整数>module.constants.compileCacheStatus
中的一个值。message
<字符串> | <未定义> 如果 Node.js 无法启用编译缓存,则包含错误消息。仅当status
为module.constants.compileCacheStatus.FAILED
时设置。directory
<字符串> | <未定义> 如果启用了编译缓存,则包含编译缓存的存储目录。仅当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 实例。要在子工作线程中启用它,也需要在子工作线程中调用此方法,或者将 process.env.NODE_COMPILE_CACHE
值设置为编译缓存目录,以便行为可以继承到子工作线程中。可以使用此方法返回的 directory
字段或使用 module.getCompileCacheDir()
获取该目录。
module.flushCompileCache()
新增于: v23.0.0
将当前 Node.js 实例中已加载模块累积的模块编译缓存刷新到磁盘。此操作在所有刷新文件系统操作结束后返回,无论它们是否成功。如果出现任何错误,此操作将静默失败,因为编译缓存未命中不应干扰应用程序的实际操作。
module.getCompileCacheDir()
新增于: v22.8.0
- 返回值: <string> | <undefined> 如果启用了模块编译缓存,则返回其目录路径;否则返回
undefined
。
自定义钩子
[历史]
版本 | 变更 |
---|---|
v23.5.0 | 添加对同步和线程内钩子的支持。 |
v20.6.0, v18.19.0 | 添加 initialize 钩子以替换 globalPreload 。 |
v18.6.0, v16.17.0 | 添加对链式加载器的支持。 |
v16.12.0 | 删除 getFormat 、getSource 、transformSource 和 globalPreload ;添加 load 钩子和 getGlobalPreload 钩子。 |
v8.8.0 | 在 v8.8.0 中添加 |
目前支持两种类型的模块自定义钩子:
启用
模块解析和加载可以通过以下方式自定义:
钩子可以在应用程序代码运行之前使用 --import
或 --require
标志注册:
node --import ./register-hooks.js ./my-app.js
node --require ./register-hooks.js ./my-app.js
// register-hooks.js
// 如果此文件不包含顶层 await,则只能 require() 它。
// 使用 module.register() 在专用线程中注册异步钩子。
import { register } from 'node:module'
register('./hooks.mjs', import.meta.url)
// register-hooks.js
const { register } = require('node:module')
const { pathToFileURL } = require('node:url')
// 使用 module.register() 在专用线程中注册异步钩子。
register('./hooks.mjs', pathToFileURL(__filename))
// 使用 module.registerHooks() 在主线程中注册同步钩子。
import { registerHooks } from 'node:module'
registerHooks({
resolve(specifier, context, nextResolve) {
/* 实现 */
},
load(url, context, nextLoad) {
/* 实现 */
},
})
// 使用 module.registerHooks() 在主线程中注册同步钩子。
const { registerHooks } = require('node:module')
registerHooks({
resolve(specifier, context, nextResolve) {
/* 实现 */
},
load(url, context, nextLoad) {
/* 实现 */
},
})
传递给 --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
可确保在导入任何应用程序文件(包括应用程序的入口点)之前注册钩子,默认情况下也适用于任何工作线程。
或者,可以从入口点调用 register()
和 registerHooks()
,尽管对于应在注册钩子后运行的任何 ESM 代码,必须使用动态 import()
。
import { register } from 'node:module'
register('http-to-https', import.meta.url)
// 由于这是一个动态 `import()`,因此 `http-to-https` 钩子将运行
// 以处理 `./my-app.js` 及其导入或需要的任何其他文件。
await import('./my-app.js')
const { register } = require('node:module')
const { pathToFileURL } = require('node:url')
register('http-to-https', pathToFileURL(__filename))
// 由于这是一个动态 `import()`,因此 `http-to-https` 钩子将运行
// 以处理 `./my-app.js` 及其导入或需要的任何其他文件。
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({
/* 同步钩子的实现 */
})
const require = createRequire(import.meta.url)
// 同步钩子会影响 import、require() 和通过 createRequire() 创建的用户 require() 函数。
await import('./my-app.js')
require('./my-app-2.js')
const { register, registerHooks } = require('node:module')
const { pathToFileURL } = require('node:url')
registerHooks({
/* 同步钩子的实现 */
})
const userRequire = createRequire(__filename)
// 同步钩子会影响 import、require() 和通过 createRequire() 创建的用户 require() 函数。
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.js 默认 ← ./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'
// 此示例演示如何使用消息通道与钩子通信,方法是将 `port2` 发送到钩子。
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')
// 此示例演示如何使用消息通道与钩子通信,方法是将 `port2` 发送到钩子。
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 }) {
// 接收来自 `register` 的数据。
}
export async function resolve(specifier, context, nextResolve) {
// 获取 `import` 或 `require` 说明符,并将其解析为 URL。
}
export async function load(url, context, nextLoad) {
// 获取解析后的 URL 并返回要评估的源代码。
}
异步钩子在一个单独的线程中运行,与运行应用程序代码的主线程隔离。这意味着它是一个不同的领域。主线程可以随时终止钩子线程,因此不要依赖异步操作(例如 console.log
)来完成。它们默认继承到子工作进程中。
module.registerHooks()
接受的同步钩子函数
新增于:v23.5.0
module.registerHooks()
方法接受同步钩子函数。initialize()
不受支持也不必要,因为钩子实现者可以在调用 module.registerHooks()
之前直接运行初始化代码。
function resolve(specifier, context, nextResolve) {
// 将 `import` 或 `require` 说明符解析为 URL。
}
function load(url, context, nextLoad) {
// 获取已解析的 URL 并返回要评估的源代码。
}
同步钩子函数在与模块加载相同的线程和相同的 领域 中运行。与异步钩子函数不同,它们默认不会继承到子工作线程中,但是,如果使用由 --import
或 --require
预加载的文件注册钩子函数,则子工作线程可以通过 process.execArgv
继承预加载的脚本。详情请参见 Worker 文档。
在同步钩子函数中,用户可以预期 console.log()
的完成方式与他们在模块代码中预期 console.log()
的完成方式相同。
Hook 的约定
Hook 是链的一部分 [/api/module#chaining],即使该链仅由一个自定义(用户提供的)Hook 和始终存在的默认 Hook 组成。Hook 函数嵌套:每个函数都必须始终返回一个普通对象,并且链式调用是通过每个函数调用 next<hookName>()
实现的,它指向后续加载器的 Hook(后进先出顺序)。
返回的值缺少必需属性的 Hook 会触发异常。不调用 next<hookName>()
且不返回 shortCircuit: true
的 Hook 也会触发异常。这些错误是为了帮助防止链中意外中断。从 Hook 返回 shortCircuit: true
表示链有意在您的 Hook 处结束。
initialize()
新增于:v20.6.0, v18.19.0
data
<任何> 来自register(loader, import.meta.url, { data })
的数据。
initialize
Hook 仅被 register
接受。registerHooks()
不支持也不需要它,因为同步 Hook 的初始化可以在调用 registerHooks()
之前直接运行。
initialize
Hook 提供了一种方法来定义一个自定义函数,该函数在初始化 Hooks 模块时在 Hooks 线程中运行。当通过 register
注册 Hooks 模块时会发生初始化。
此 Hook 可以接收来自 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'
// 此示例展示了如何使用消息通道在主(应用程序)线程和在 Hooks 线程上运行的 Hooks 之间进行通信,方法是将 `port2` 发送到 `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')
// 此示例展示了如何使用消息通道在主(应用程序)线程和在 Hooks 线程上运行的 Hooks 之间进行通信,方法是将 `port2` 发送到 `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)
[历史]
版本 | 变更 |
---|---|
v23.5.0 | 添加对同步和线程内钩子的支持。 |
v21.0.0, v20.10.0, v18.19.0 | 属性 context.importAssertions 被 context.importAttributes 替换。使用旧名称仍然受支持,但会发出实验性警告。 |
v18.6.0, v16.17.0 | 添加对链式解析钩子的支持。每个钩子必须调用 nextResolve() 或在其返回值中包含设置为 true 的 shortCircuit 属性。 |
v17.1.0, v16.14.0 | 添加对导入断言的支持。 |
specifier
<字符串>context
<对象>nextResolve
<函数> 链中的后续resolve
钩子,或者在最后一个用户提供的resolve
钩子之后的 Node.js 默认resolve
钩子返回值: <对象> | <Promise> 异步版本采用包含以下属性的对象,或解析为该对象的
Promise
。同步版本仅接受同步返回的对象。
resolve
钩子链负责告诉 Node.js 在哪里查找以及如何缓存给定的 import
语句或表达式,或 require
调用。它可以选择返回一个格式(例如 'module'
)作为对 load
钩子的提示。如果指定了格式,则 load
钩子最终负责提供最终的 format
值(并且可以自由忽略 resolve
提供的提示);如果 resolve
提供了 format
,则即使仅将值传递给 Node.js 默认 load
钩子,也需要自定义 load
钩子。
导入类型属性是将加载的模块保存到内部模块缓存的缓存键的一部分。如果模块应使用与源代码中不同的属性进行缓存,则 resolve
钩子负责返回 importAttributes
对象。
context
中的 conditions
属性是一个条件数组,这些条件将用于匹配此解析请求的包导出条件。它们可用于在其他地方查找条件映射或在调用默认解析逻辑时修改列表。
当前的包导出条件 始终位于传递到钩子的 context.conditions
数组中。为了保证在调用 defaultResolve
时默认的 Node.js 模块说明符解析行为,传递给它的 context.conditions
数组必须包含最初传递到 resolve
钩子的 context.conditions
数组中的所有元素。
// module.register() 接受的异步版本。
export async function resolve(specifier, context, nextResolve) {
const { parentURL = null } = context
if (Math.random() > 0.5) {
// 某些条件。
// 对于某些或所有说明符,执行一些自定义的解析逻辑。
// 始终返回 {url: <string>} 格式的对象。
return {
shortCircuit: true,
url: parentURL ? new URL(specifier, parentURL).href : new URL(specifier).href,
}
}
if (Math.random() < 0.5) {
// 另一个条件。
// 调用 `defaultResolve` 时,参数可以修改。在这种情况下,它会添加另一个值来匹配条件导出。
return nextResolve(specifier, {
...context,
conditions: [...context.conditions, 'another-condition'],
})
}
// 推迟到链中的下一个钩子,如果这是最后一个用户指定的加载器,则为 Node.js 默认解析。
return nextResolve(specifier)
}
// module.registerHooks() 接受的同步版本。
function resolve(specifier, context, nextResolve) {
// 与上面的异步 resolve() 类似,因为它没有任何异步逻辑。
}
load(url, context, nextLoad)
[历史]
版本 | 变更 |
---|---|
v23.5.0 | 添加对同步和线程内版本的支持。 |
v20.6.0 | 添加对格式为 commonjs 的 source 的支持。 |
v18.6.0, v16.17.0 | 添加对链式加载钩子的支持。每个钩子必须调用 nextLoad() 或在其返回结果中包含设置为 true 的 shortCircuit 属性。 |
url
<字符串>resolve
链返回的 URLcontext
<对象>nextLoad
<函数> 链中的后续load
钩子,或最后一个用户提供的load
钩子之后的 Node.js 默认load
钩子返回值: <对象> | <Promise> 异步版本接受包含以下属性的对象,或解析为该对象的
Promise
。同步版本只接受同步返回的对象。format
<字符串>shortCircuit
<未定义> | <布尔值> 一个信号,表示此钩子意图终止load
钩子链。默认值:false
source
<字符串> | <ArrayBuffer> | <TypedArray> Node.js 要评估的源代码
load
钩子提供了一种自定义方法来确定如何解释、检索和解析 URL。它还负责验证导入属性。
format
的最终值必须是以下值之一:
format | 描述 | load 返回的 source 的可接受类型 |
---|---|---|
'builtin' | 加载 Node.js 内置模块 | 不适用 |
'commonjs' | 加载 Node.js CommonJS 模块 | { 字符串 , ArrayBuffer , TypedArray , null , undefined } |
'json' | 加载 JSON 文件 | { 字符串 , ArrayBuffer , TypedArray } |
'module' | 加载 ES 模块 | { 字符串 , ArrayBuffer , TypedArray } |
'wasm' | 加载 WebAssembly 模块 | { ArrayBuffer , TypedArray } |
对于 'builtin'
类型,source
的值会被忽略,因为目前无法替换 Node.js 内置(核心)模块的值。
异步 load
钩子中的警告
使用异步 load
钩子时,为 'commonjs'
提供 source
与不提供 source
的效果截然不同:
- 提供
source
时,来自此模块的所有require
调用都将由注册了resolve
和load
钩子的 ESM 加载器处理;来自此模块的所有require.resolve
调用都将由注册了resolve
钩子的 ESM 加载器处理;只有部分 CommonJS API 可用(例如,没有require.extensions
、没有require.cache
、没有require.resolve.paths
),并且对 CommonJS 模块加载器的猴子补丁将不适用。 - 如果
source
未定义或为null
,它将由 CommonJS 模块加载器处理,并且require
/require.resolve
调用将不会经过注册的钩子。这种针对空值source
的行为是暂时的——将来将不支持空值source
。
这些警告不适用于同步 load
钩子,在这种情况下,自定义 CommonJS 模块可以使用完整的 CommonJS API 集,并且 require
/require.resolve
始终经过注册的钩子。
Node.js 内部异步 load
实现(这是 load
链中最后一个钩子的 next
值)在 format
为 'commonjs'
时返回 source
为 null
,以保持向后兼容性。这是一个选择使用非默认行为的示例钩子:
import { readFile } from 'node:fs/promises'
// module.register() 接受的异步版本。对于 module.registerSync() 接受的同步版本,不需要此修复。
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
包含由下一个钩子加载的源代码,而不管模块格式如何。
- 特定的
ArrayBuffer
对象是SharedArrayBuffer
。 - 特定的
TypedArray
对象是Uint8Array
。
如果基于文本的格式(即 'json'
、'module'
)的 source 值不是字符串,则使用 util.TextDecoder
将其转换为字符串。
load
钩子提供了一种自定义方法来检索已解析 URL 的源代码。这允许加载器潜在地避免从磁盘读取文件。它还可以用于将无法识别的格式映射到受支持的格式,例如将 yaml
映射到 module
。
// module.register() 接受的异步版本。
export async function load(url, context, nextLoad) {
const { format } = context
if (Math.random() > 0.5) {
// 一些条件
/*
对于某些或所有 URL,执行一些自定义逻辑来检索源代码。
始终返回以下形式的对象:
{
format: <字符串>,
source: <字符串|缓冲区>,
}.
*/
return {
format,
shortCircuit: true,
source: '...',
}
}
// 将其委托给链中的下一个钩子。
return nextLoad(url)
}
// module.registerHooks() 接受的同步版本。
function load(url, context, nextLoad) {
// 与上面的异步 load() 类似,因为它没有任何异步逻辑。
}
在更高级的场景中,这也可以用于将不受支持的源代码转换为受支持的源代码(参见下面的 示例)。
示例
各种模块自定义钩子可以组合使用,以实现对 Node.js 代码加载和评估行为的广泛自定义。
从 HTTPS 导入
下面的钩子注册了钩子以启用对这些标识符的基本支持。虽然这似乎是对 Node.js 核心功能的重大改进,但实际使用这些钩子存在很大的缺点:性能比从磁盘加载文件慢得多,没有缓存,也没有安全性。
// https-hooks.mjs
import { get } from 'node:https'
export function load(url, context, nextLoad) {
// 为了通过网络加载 JavaScript,我们需要获取并返回它。
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({
// 此示例假设所有网络提供的 JavaScript 都是 ES 模块代码。
format: 'module',
shortCircuit: true,
source: data,
})
)
}).on('error', err => reject(err))
})
}
// 让 Node.js 处理所有其他 URL。
return nextLoad(url)
}
// main.mjs
import { VERSION } from 'https://coffeescript.org/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 { dirname, extname, resolve as resolvePath } from 'node:path'
import { cwd } from 'node:process'
import { fileURLToPath, pathToFileURL } from 'node:url'
import coffeescript from 'coffeescript'
const extensionsRegex = /\.(coffee|litcoffee|coffee\.md)$/
export async function load(url, context, nextLoad) {
if (extensionsRegex.test(url)) {
// CoffeeScript 文件可以是 CommonJS 或 ES 模块,因此我们希望 Node.js 将任何
// CoffeeScript 文件与同一位置的 .js 文件的处理方式相同。为了确定 Node.js 如何解释任意 .js
// 文件,请向上搜索文件系统以查找最近的父 package.json 文件
// 并读取其“type”字段。
const format = await getPackageType(url)
const { source: rawSource } = await nextLoad(url, { ...context, format })
// 此钩子将所有导入的 CoffeeScript 文件的 CoffeeScript 源代码转换为 JavaScript 源代码。
const transformedSource = coffeescript.compile(rawSource.toString(), url)
return {
format,
shortCircuit: true,
source: transformedSource,
}
}
// 让 Node.js 处理所有其他 URL。
return nextLoad(url)
}
async function getPackageType(url) {
// `url` 仅在第一次迭代时作为从 load() 钩子传递的解析 url 的文件路径
// 来自 load() 的实际文件路径将包含文件扩展名,因为这是规范要求的
// 此简单的真值检查(检查 `url` 是否包含文件扩展名)
// 适用于大多数项目,但不涵盖某些极端情况(例如
// 无扩展名文件或以尾随空格结尾的 url)
const isFilePath = !!extname(url)
// 如果它是一个文件路径,则获取它所在的目录
const dir = isFilePath ? dirname(fileURLToPath(url)) : url
// 组成同一目录中 package.json 的文件路径,
// 可能存在也可能不存在
const packagePath = resolvePath(dir, 'package.json')
// 尝试读取可能不存在的 package.json
const type = await readFile(packagePath, { encoding: 'utf8' })
.then(filestring => JSON.parse(filestring).type)
.catch(err => {
if (err?.code !== 'ENOENT') console.error(err)
})
// 如果 package.json 存在且包含具有值的 `type` 字段,则大功告成
if (type) return type
// 否则,(如果不在根目录)继续检查下一个上级目录
// 如果在根目录,则停止并返回 false
return dir.length > 1 && getPackageType(resolvePath(dir, '..'))
}
同步版本
// coffeescript-sync-hooks.mjs
import { readFileSync } from 'node:fs/promises'
import { registerHooks } from 'node:module'
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 extensionsRegex = /\.(coffee|litcoffee|coffee\.md)$/
function load(url, context, nextLoad) {
if (extensionsRegex.test(url)) {
const format = getPackageType(url)
const { source: rawSource } = nextLoad(url, { ...context, format })
const transformedSource = coffeescript.compile(rawSource.toString(), url)
return {
format,
shortCircuit: true,
source: transformedSource,
}
}
return nextLoad(url)
}
function getPackageType(url) {
const isFilePath = !!extname(url)
const dir = isFilePath ? dirname(fileURLToPath(url)) : url
const packagePath = resolvePath(dir, 'package.json')
let type
try {
const filestring = readFileSync(packagePath, { encoding: 'utf8' })
type = JSON.parse(filestring).type
} catch (err) {
if (err?.code !== 'ENOENT') console.error(err)
}
if (type) return type
return dir.length > 1 && getPackageType(resolvePath(dir, '..'))
}
registerHooks({ load })
运行钩子
# main.coffee {#maincoffee}
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 {#screamcoffee}
export scream = (str) -> str.toUpperCase()
使用上述钩子模块,运行 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-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!
。
源映射 v3 支持
新增于:v13.7.0, v12.17.0
用于与源映射缓存交互的辅助函数。当启用源映射解析并且在模块的页脚中找到源映射包含指令时,将填充此缓存。
要启用源映射解析,必须使用标志--enable-source-maps
运行 Node.js,或者通过设置NODE_V8_COVERAGE=dir
启用代码覆盖率。
// module.mjs
// 在 ECMAScript 模块中
import { findSourceMap, SourceMap } from 'node:module'
// module.cjs
// 在 CommonJS 模块中
const { findSourceMap, SourceMap } = require('node:module')
module.findSourceMap(path)
新增于:v13.7.0, v12.17.0
path
<string>- 返回值:<module.SourceMap> | <undefined> 如果找到源映射,则返回
module.SourceMap
,否则返回undefined
。
path
是应为其获取相应源映射的文件的已解析路径。
类: module.SourceMap
新增于: v13.7.0, v12.17.0
new SourceMap(payload[, { lineLengths }])
{#new-sourcemappayload-{-linelengths-}}
创建一个新的 sourceMap
实例。
payload
是一个对象,其键与 Source map v3 格式 匹配:
file
: <字符串>version
: <数字>sources
: <字符串数组>sourcesContent
: <字符串数组>names
: <字符串数组>mappings
: <字符串>sourceRoot
: <字符串>
lineLengths
是一个可选数组,包含生成的代码中每一行的长度。
sourceMap.payload
- 返回值: <对象>
用于构建 SourceMap
实例的有效负载的 Getter。
sourceMap.findEntry(lineOffset, columnOffset)
给定生成的源文件中的一行偏移量和一列偏移量,如果找到,则返回表示原始文件中 SourceMap 范围的对象;否则返回空对象。
返回的对象包含以下键:
- generatedLine: <数字> 生成的源代码中范围起点的行偏移量
- generatedColumn: <数字> 生成的源代码中范围起点的列偏移量
- originalSource: <字符串> 原始源文件的名称,如 SourceMap 中所报告
- originalLine: <数字> 原始源代码中范围起点的行偏移量
- originalColumn: <数字> 原始源代码中范围起点的列偏移量
- name: <字符串>
返回值表示 SourceMap 中出现的原始范围,基于从零开始的偏移量,不是 错误消息和 CallSite 对象中显示的从 1 开始的行号和列号。
要从 Error 堆栈和 CallSite 对象报告的 lineNumber 和 columnNumber 获取相应的从 1 开始的行号和列号,请使用 sourceMap.findOrigin(lineNumber, columnNumber)
sourceMap.findOrigin(lineNumber, columnNumber)
lineNumber
<number> 生成的源代码中调用位置的 1-indexed 行号columnNumber
<number> 生成的源代码中调用位置的 1-indexed 列号- 返回值: <Object>
给定生成的源代码中调用位置的 1-indexed lineNumber
和 columnNumber
,查找原始源代码中对应的调用位置。
如果提供的 lineNumber
和 columnNumber
在任何源映射中都找不到,则返回一个空对象。否则,返回的对象包含以下键:
- name: <string> | <undefined> 源映射中范围的名称(如果提供)。
- fileName: <string> 源映射中报告的原始源文件的名称
- lineNumber: <number> 原始源代码中对应调用位置的 1-indexed lineNumber
- columnNumber: <number> 原始源代码中对应调用位置的 1-indexed columnNumber