Node.js v26.0.0 文档
- Node.js v26.0.0
- 目录
- VM (执行 JavaScript)
- 类:
vm.Script - 类:
vm.Module - 类:
vm.SourceTextModule - 类:
vm.SyntheticModule - 类型:
ModuleRequest vm.compileFunction(code[, params[, options]])vm.constantsvm.createContext([contextObject[, options]])vm.isContext(object)vm.measureMemory([options])vm.runInContext(code, contextifiedObject[, options])vm.runInNewContext(code[, contextObject[, options]])vm.runInThisContext(code[, options])- 示例:在虚拟机(VM)内运行 HTTP 服务器
- “上下文化”(contextify)一个对象是什么意思?
- 超时与异步任务及 Promise 的交互
- 编译 API 对动态
import()的支持
- 类:
- VM (执行 JavaScript)
- 索引
- 关于本文档
- 用法与示例
- 断言测试
- 异步上下文跟踪
- 异步钩子
- 缓冲区
- C++ 插件
- 使用 Node-API 的 C/C++ 插件
- C++ 嵌入器 API
- 子进程
- 集群
- 命令行选项
- 控制台
- 加密
- 调试器
- 已弃用的 API
- 诊断通道
- DNS
- 域
- 环境变量
- 错误
- 事件
- 文件系统
- 全局对象
- HTTP
- HTTP/2
- HTTPS
- 检查器
- 国际化
- 模块:CommonJS 模块
- 模块:ECMAScript 模块
- 模块:
node:moduleAPI - 模块:包
- 模块:TypeScript
- 网络
- 可迭代流 API
- 操作系统
- 路径
- 性能钩子
- 权限
- 进程
- Punycode
- 查询字符串
- 逐行读取
- REPL
- 报告
- 单一可执行文件应用
- SQLite
- 流
- 字符串解码器
- 测试运行器
- 定时器
- TLS/SSL
- 跟踪事件
- TTY
- UDP/数据报
- URL
- 实用工具
- V8
- 虚拟机
- WASI
- Web Crypto API
- Web Streams API
- 工作线程
- Zlib
- Zlib 可迭代压缩
- 其他版本
- 选项
VM (JavaScript 执行)#
稳定性:2 - 稳定
node:vm 模块允许在 V8 虚拟机上下文中编译和运行代码。
node:vm 模块不是安全机制。请勿使用它来运行不可信代码。
JavaScript 代码可以立即编译并运行,也可以编译后保存,留待稍后运行。
一个常见的用例是在不同的 V8 上下文中运行代码。这意味着被调用的代码拥有与调用代码不同的全局对象。
可以通过对对象进行上下文化来提供上下文。被调用的代码将上下文中的任何属性视为全局变量。由被调用代码引起的任何全局变量更改都会反映在上下文对象中。
import { createContext, runInContext } from 'node:vm'; const x = 1; const context = { x: 2 }; createContext(context); // Contextify the object. const code = 'x += 40; var y = 17;'; // `x` and `y` are global variables in the context. // Initially, x has the value 2 because that is the value of context.x. runInContext(code, context); console.log(context.x); // 42 console.log(context.y); // 17 console.log(x); // 1; y is not definedconst { createContext, runInContext } = require('node:vm'); const x = 1; const context = { x: 2 }; createContext(context); // Contextify the object. const code = 'x += 40; var y = 17;'; // `x` and `y` are global variables in the context. // Initially, x has the value 2 because that is the value of context.x. runInContext(code, context); console.log(context.x); // 42 console.log(context.y); // 17 console.log(x); // 1; y is not defined
类:vm.Script#
vm.Script 类的实例包含可在特定上下文中执行的预编译脚本。
new vm.Script(code[, options])#
code<string>要编译的 JavaScript 代码。options<Object>|<string>filename<string>指定此脚本生成的堆栈跟踪中使用的文件名。默认值:'evalmachine.<anonymous>'。lineOffset<number>指定此脚本生成的堆栈跟踪中显示的行号偏移量。默认值:0。columnOffset<number>指定此脚本生成的堆栈跟踪中显示的第一行列号偏移量。默认值:0。cachedData<Buffer>|<TypedArray>|<DataView>为提供的源代码提供可选的Buffer、TypedArray或DataView,其中包含 V8 的代码缓存数据。提供此数据后,cachedDataRejected的值将根据 V8 是否接受该数据被设置为true或false。produceCachedData<boolean>当为true且不存在cachedData时,V8 将尝试为code生成代码缓存数据。成功后,包含 V8 代码缓存数据的Buffer将被生成并存储在返回的vm.Script实例的cachedData属性中。cachedDataProduced的值将根据代码缓存数据是否成功生成被设置为true或false。此选项已弃用,建议改用script.createCachedData()。默认值:false。importModuleDynamically<Function>|<vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER>用于指定在评估此脚本期间调用import()时应如何加载模块。此选项是实验性模块 API 的一部分。我们不建议在生产环境中使用它。详细信息请参阅编译 API 对动态import()的支持。
如果 options 是字符串,则指定文件名。
创建新的 vm.Script 对象会编译 code 但不会运行它。编译后的 vm.Script 可以在稍后多次运行。code 不绑定到任何全局对象;相反,它在每次运行前都会绑定,仅针对那次运行。
script.cachedDataRejected#
- 类型:
<boolean>|<undefined>
当创建 vm.Script 时提供了 cachedData,此值将根据 V8 是否接受该数据被设置为 true 或 false。否则该值为 undefined。
script.createCachedData()#
- 返回:
<Buffer>
创建一个可用于 Script 构造函数 cachedData 选项的代码缓存。返回一个 Buffer。此方法可以在任何时间、任意次数调用。
Script 的代码缓存不包含任何 JavaScript 可观察状态。代码缓存可以安全地与脚本源代码一起保存,并用于多次构建新的 Script 实例。
Script 源码中的函数可以被标记为延迟编译,它们在 Script 构建时不会被编译。这些函数将在首次调用时被编译。代码缓存序列化了 V8 当前已知的关于该 Script 的元数据,可用于加速未来的编译。
const script = new vm.Script(`
function add(a, b) {
return a + b;
}
const x = add(1, 2);
`);
const cacheWithoutAdd = script.createCachedData();
// In `cacheWithoutAdd` the function `add()` is marked for full compilation
// upon invocation.
script.runInThisContext();
const cacheWithAdd = script.createCachedData();
// `cacheWithAdd` contains fully compiled function `add()`.
script.runInContext(contextifiedObject[, options])#
contextifiedObject<Object>由vm.createContext()方法返回的已上下文化的对象。options<Object>- 返回:
<any>脚本中执行的最后一条语句的结果。
在给定的 contextifiedObject 中运行 vm.Script 对象中包含的已编译代码,并返回结果。运行的代码无法访问本地作用域。
以下示例编译了递增全局变量的代码,设置了另一个全局变量的值,然后多次执行该代码。全局变量包含在 context 对象中。
import { createContext, Script } from 'node:vm'; const context = { animal: 'cat', count: 2, }; const script = new Script('count += 1; name = "kitty";'); createContext(context); for (let i = 0; i < 10; ++i) { script.runInContext(context); } console.log(context); // Prints: { animal: 'cat', count: 12, name: 'kitty' }const { createContext, Script } = require('node:vm'); const context = { animal: 'cat', count: 2, }; const script = new Script('count += 1; name = "kitty";'); createContext(context); for (let i = 0; i < 10; ++i) { script.runInContext(context); } console.log(context); // Prints: { animal: 'cat', count: 12, name: 'kitty' }
使用 timeout 或 breakOnSigint 选项将导致启动新的事件循环和相应的线程,这会有非零的性能开销。
script.runInNewContext([contextObject[, options]])#
contextObject<Object>|<vm.constants.DONT_CONTEXTIFY>|<undefined>要么是vm.constants.DONT_CONTEXTIFY,要么是一个将被上下文化的对象。如果为undefined,将创建一个空的已上下文化对象以实现向后兼容。options<Object>displayErrors<boolean>当为true时,如果编译code时发生Error,则导致错误的这一行代码将被附加到堆栈跟踪中。默认值:true。timeout<integer>指定在终止执行前执行code的毫秒数。如果执行被终止,将抛出Error。该值必须是严格的正整数。breakOnSigint<boolean>如果为true,接收SIGINT(Ctrl+C) 将终止执行并抛出Error。通过process.on('SIGINT')附加的现有事件处理程序在脚本执行期间会被禁用,但在此之后会继续生效。默认值:false。contextName<string>新创建上下文的可读名称。默认值:'VM Context i',其中i是创建上下文的递增数字索引。contextOrigin<string>用于显示目的、与新创建上下文相对应的源(Origin)。源应格式化为 URL,但仅包含方案、主机和端口(如有必要),如URL对象的url.origin属性值。最重要的是,此字符串应省略尾随斜杠,因为它表示路径。默认值:''。contextCodeGeneration<Object>microtaskMode<string>如果设为afterEvaluate,微任务(通过Promise和async function调度的任务)将在脚本运行后立即运行。在这种情况下,它们会被包含在timeout和breakOnSigint的作用域中。
- 返回:
<any>脚本中执行的最后一条语句的结果。
此方法是 script.runInContext(vm.createContext(options), options) 的快捷方式。它同时执行多项操作:
- 创建一个新上下文。
- 如果
contextObject是对象,则使用新上下文将其上下文化。如果contextObject未定义,则创建一个新对象并将其上下文化。如果contextObject是vm.constants.DONT_CONTEXTIFY,则不上下文化任何东西。 - 在创建的上下文中运行
vm.Script对象中包含的已编译代码。该代码无法访问调用此方法的作用域。 - 返回结果。
以下示例编译了设置全局变量的代码,然后在不同的上下文中多次执行该代码。全局变量在每个单独的 context 上设置并包含在其中。
import { constants, Script } from 'node:vm'; const script = new Script('globalVar = "set"'); const contexts = [{}, {}, {}]; contexts.forEach((context) => { script.runInNewContext(context); }); console.log(contexts); // Prints: [{ globalVar: 'set' }, { globalVar: 'set' }, { globalVar: 'set' }] // This would throw if the context is created from a contextified object. // constants.DONT_CONTEXTIFY allows creating contexts with ordinary // global objects that can be frozen. const freezeScript = new Script('Object.freeze(globalThis); globalThis;'); const frozenContext = freezeScript.runInNewContext(constants.DONT_CONTEXTIFY);const { constants, Script } = require('node:vm'); const script = new Script('globalVar = "set"'); const contexts = [{}, {}, {}]; contexts.forEach((context) => { script.runInNewContext(context); }); console.log(contexts); // Prints: [{ globalVar: 'set' }, { globalVar: 'set' }, { globalVar: 'set' }] // This would throw if the context is created from a contextified object. // constants.DONT_CONTEXTIFY allows creating contexts with ordinary // global objects that can be frozen. const freezeScript = new Script('Object.freeze(globalThis); globalThis;'); const frozenContext = freezeScript.runInNewContext(constants.DONT_CONTEXTIFY);
script.runInThisContext([options])#
在当前 global 对象的上下文中运行 vm.Script 对象中包含的已编译代码。运行的代码无法访问本地作用域,但可以访问当前的 global 对象。
以下示例编译了递增 global 变量的代码,然后多次执行该代码。
import { Script } from 'node:vm'; global.globalVar = 0; const script = new Script('globalVar += 1', { filename: 'myfile.vm' }); for (let i = 0; i < 1000; ++i) { script.runInThisContext(); } console.log(globalVar); // 1000const { Script } = require('node:vm'); global.globalVar = 0; const script = new Script('globalVar += 1', { filename: 'myfile.vm' }); for (let i = 0; i < 1000; ++i) { script.runInThisContext(); } console.log(globalVar); // 1000
script.sourceMapURL#
- 类型:
<string>|<undefined>
当脚本从包含源映射(source map)魔术注释的源编译而来时,此属性将被设置为源映射的 URL。
import vm from 'node:vm'; const script = new vm.Script(` function myFunc() {} //# sourceMappingURL=sourcemap.json `); console.log(script.sourceMapURL); // Prints: sourcemap.jsonconst vm = require('node:vm'); const script = new vm.Script(` function myFunc() {} //# sourceMappingURL=sourcemap.json `); console.log(script.sourceMapURL); // Prints: sourcemap.json
类:vm.Module#
稳定性:1 - 实验性
此功能仅在启用了 --experimental-vm-modules 命令标志时可用。
vm.Module 类提供了一个用于在 VM 上下文中使用 ECMAScript 模块的底层接口。它是 vm.Script 类的对应物,紧密映射了 ECMAScript 规范中定义的模块记录(Module Record)。
然而,与 vm.Script 不同,每个 vm.Module 对象从创建起就绑定到一个上下文。
使用 vm.Module 对象需要三个不同的步骤:创建/解析、链接和评估。以下示例说明了这三个步骤。
此实现处于比 ECMAScript 模块加载器 更底层的级别。目前还没有办法与加载器交互,但计划提供支持。
import vm from 'node:vm'; const contextifiedObject = vm.createContext({ secret: 42, print: console.log, }); // Step 1 // // Create a Module by constructing a new `vm.SourceTextModule` object. This // parses the provided source text, throwing a `SyntaxError` if anything goes // wrong. By default, a Module is created in the top context. But here, we // specify `contextifiedObject` as the context this Module belongs to. // // Here, we attempt to obtain the default export from the module "foo", and // put it into local binding "secret". const rootModule = new vm.SourceTextModule(` import s from 'foo'; s; print(s); `, { context: contextifiedObject }); // Step 2 // // "Link" the imported dependencies of this Module to it. // // Obtain the requested dependencies of a SourceTextModule by // `sourceTextModule.moduleRequests` and resolve them. // // Even top-level Modules without dependencies must be explicitly linked. The // array passed to `sourceTextModule.linkRequests(modules)` can be // empty, however. // // Note: This is a contrived example in that the resolveAndLinkDependencies // creates a new "foo" module every time it is called. In a full-fledged // module system, a cache would probably be used to avoid duplicated modules. const moduleMap = new Map([ ['root', rootModule], ]); function resolveAndLinkDependencies(module) { const requestedModules = module.moduleRequests.map((request) => { // In a full-fledged module system, the resolveAndLinkDependencies would // resolve the module with the module cache key `[specifier, attributes]`. // In this example, we just use the specifier as the key. const specifier = request.specifier; let requestedModule = moduleMap.get(specifier); if (requestedModule === undefined) { requestedModule = new vm.SourceTextModule(` // The "secret" variable refers to the global variable we added to // "contextifiedObject" when creating the context. export default secret; `, { context: module.context }); moduleMap.set(specifier, requestedModule); // Resolve the dependencies of the new module as well. resolveAndLinkDependencies(requestedModule); } return requestedModule; }); module.linkRequests(requestedModules); } resolveAndLinkDependencies(rootModule); rootModule.instantiate(); // Step 3 // // Evaluate the Module. The evaluate() method returns a promise which will // resolve after the module has finished evaluating. // Prints 42. await rootModule.evaluate();const vm = require('node:vm'); const contextifiedObject = vm.createContext({ secret: 42, print: console.log, }); (async () => { // Step 1 // // Create a Module by constructing a new `vm.SourceTextModule` object. This // parses the provided source text, throwing a `SyntaxError` if anything goes // wrong. By default, a Module is created in the top context. But here, we // specify `contextifiedObject` as the context this Module belongs to. // // Here, we attempt to obtain the default export from the module "foo", and // put it into local binding "secret". const rootModule = new vm.SourceTextModule(` import s from 'foo'; s; print(s); `, { context: contextifiedObject }); // Step 2 // // "Link" the imported dependencies of this Module to it. // // Obtain the requested dependencies of a SourceTextModule by // `sourceTextModule.moduleRequests` and resolve them. // // Even top-level Modules without dependencies must be explicitly linked. The // array passed to `sourceTextModule.linkRequests(modules)` can be // empty, however. // // Note: This is a contrived example in that the resolveAndLinkDependencies // creates a new "foo" module every time it is called. In a full-fledged // module system, a cache would probably be used to avoid duplicated modules. const moduleMap = new Map([ ['root', rootModule], ]); function resolveAndLinkDependencies(module) { const requestedModules = module.moduleRequests.map((request) => { // In a full-fledged module system, the resolveAndLinkDependencies would // resolve the module with the module cache key `[specifier, attributes]`. // In this example, we just use the specifier as the key. const specifier = request.specifier; let requestedModule = moduleMap.get(specifier); if (requestedModule === undefined) { requestedModule = new vm.SourceTextModule(` // The "secret" variable refers to the global variable we added to // "contextifiedObject" when creating the context. export default secret; `, { context: module.context }); moduleMap.set(specifier, requestedModule); // Resolve the dependencies of the new module as well. resolveAndLinkDependencies(requestedModule); } return requestedModule; }); module.linkRequests(requestedModules); } resolveAndLinkDependencies(rootModule); rootModule.instantiate(); // Step 3 // // Evaluate the Module. The evaluate() method returns a promise which will // resolve after the module has finished evaluating. // Prints 42. await rootModule.evaluate(); })();
module.error#
- 类型:
<any>
如果 module.status 为 'errored',此属性包含模块在评估期间抛出的异常。如果状态为其他任何值,访问此属性将导致抛出异常。
在没有抛出异常的情况下,不能使用 undefined 值,因为这可能与 throw undefined; 产生歧义。
对应于 ECMAScript 规范中循环模块记录(Cyclic Module Record)的 [[EvaluationError]] 字段。
module.evaluate([options])#
评估模块及其依赖项。对应于 ECMAScript 规范中循环模块记录(Cyclic Module Record)的 Evaluate() 具体方法字段。
如果模块是 vm.SourceTextModule,则必须在实例化模块后调用 evaluate();否则 evaluate() 将返回一个拒绝的 Promise。
对于 vm.SourceTextModule,evaluate() 返回的 Promise 可以同步或异步完成:
- 如果
vm.SourceTextModule本身或其任何依赖项中没有顶级await,则 Promise 将在模块及其所有依赖项评估完成后同步完成。- 如果评估成功,Promise 将同步解析为
undefined。 - 如果评估导致异常,Promise 将同步拒绝并抛出导致评估失败的异常,这与
module.error相同。
- 如果评估成功,Promise 将同步解析为
- 如果
vm.SourceTextModule本身或其任何依赖项中有顶级await,则 Promise 将在模块及其所有依赖项评估完成后异步完成。- 如果评估成功,Promise 将异步解析为
undefined。 - 如果评估导致异常,Promise 将异步拒绝并抛出导致评估失败的异常。
- 如果评估成功,Promise 将异步解析为
如果模块是 vm.SyntheticModule,evaluate() 总是返回一个同步完成的 Promise,请参阅 合成模块记录的 Evaluate() 规范。
- 如果传递给其构造函数的
evaluateCallback同步抛出异常,evaluate()将返回一个被该异常同步拒绝的 Promise。 - 如果
evaluateCallback未抛出异常,evaluate()将返回一个被同步解析为undefined的 Promise。
vm.SyntheticModule 的 evaluateCallback 在 evaluate() 调用中同步执行,其返回值会被丢弃。这意味着如果 evaluateCallback 是一个异步函数,evaluate() 返回的 Promise 将不会反映其异步行为,且来自异步 evaluateCallback 的任何拒绝都将丢失。
evaluate() 也可以在模块已经被评估后再次调用,在这种情况下:
- 如果初始评估成功(
module.status为'evaluated'),它将不执行任何操作并返回一个解析为undefined的 Promise。 - 如果初始评估导致异常(
module.status为'errored'),它将重新拒绝初始评估导致的异常。
此方法不能在模块正在评估(module.status 为 'evaluating')时调用。
module.identifier#
- 类型:
<string>
当前模块的标识符,在构造函数中设置。
module.link(linker)#
linker<Function>-
specifier<string>所请求模块的说明符import foo from 'foo'; // ^^^^^ the module specifier -
referencingModule<vm.Module>调用link()的Module对象。 -
extra<Object> -
返回:
<vm.Module>|<Promise>
-
- 返回:
<Promise>
链接模块依赖项。必须在评估前调用此方法,且每个模块只能调用一次。
使用 sourceTextModule.linkRequests(modules) 和 sourceTextModule.instantiate() 同步或异步链接模块。
该函数应返回一个 Module 对象或最终解析为 Module 对象的 Promise。返回的 Module 必须满足以下两个不变量:
- 它必须属于与父
Module相同的上下文。 - 其
status不能为'errored'。
如果返回的 Module 的 status 为 'unlinked',此方法将使用相同的提供的 linker 函数递归地在返回的 Module 上调用。
link() 返回一个 Promise,当所有链接实例解析为有效的 Module 时,该 Promise 将被解析;如果 linker 函数抛出异常或返回无效的 Module,则会被拒绝。
linker 函数大致对应于 ECMAScript 规范中实现定义的 HostResolveImportedModule 抽象操作,但有几个关键区别:
- linker 函数允许是异步的,而 HostResolveImportedModule 是同步的。
模块链接期间实际使用的 HostResolveImportedModule 实现是返回链接期间链接的模块的实现。由于此时所有模块都已完全链接,因此根据规范,HostResolveImportedModule 实现是完全同步的。
对应于 ECMAScript 规范中循环模块记录(Cyclic Module Record)的 Link() 具体方法字段。
module.namespace#
- 类型:
<Object>
模块的命名空间对象。这仅在链接(module.link())完成后可用。
对应于 ECMAScript 规范中的 GetModuleNamespace 抽象操作。
module.status#
- 类型:
<string>
模块的当前状态。将是以下值之一:
-
'unlinked':尚未调用module.link()。 -
'linking':已调用module.link(),但 linker 函数返回的所有 Promise 尚未全部解析。 -
'linked':模块已成功链接,其所有依赖项也都已链接,但尚未调用module.evaluate()。 -
'evaluating':模块正在通过其自身或父模块上的module.evaluate()进行评估。 -
'evaluated':模块已成功评估。 -
'errored':模块已评估,但抛出了异常。
除 'errored' 外,此状态字符串对应于规范的循环模块记录的 [[Status]] 字段。'errored' 对应于规范中的 'evaluated',但 [[EvaluationError]] 设置为非 undefined 的值。
类:vm.SourceTextModule#
稳定性:1 - 实验性
此功能仅在启用了 --experimental-vm-modules 命令标志时可用。
- 继承自:
<vm.Module>
vm.SourceTextModule 类提供 ECMAScript 规范中定义的源代码文本模块记录(Source Text Module Record)。
new vm.SourceTextModule(code[, options])#
code<string>要解析的 JavaScript 模块代码optionsidentifier<string>用于堆栈跟踪的字符串。默认值:'vm:module(i)',其中i是上下文相关的递增索引。cachedData<Buffer>|<TypedArray>|<DataView>为提供的源提供可选的Buffer、TypedArray或DataView,其中包含 V8 的代码缓存数据。code必须与创建此cachedData的模块相同。context<Object>由vm.createContext()方法返回的已上下文化的对象,用于在此对象中编译和评估此Module。如果未指定上下文,则在当前执行上下文中评估模块。lineOffset<integer>指定此Module生成的堆栈跟踪中显示的行号偏移量。默认值:0。columnOffset<integer>指定此Module生成的堆栈跟踪中显示的第一行列号偏移量。默认值:0。initializeImportMeta<Function>在评估此Module期间被调用,用于初始化import.meta。meta<import.meta>module<vm.SourceTextModule>
importModuleDynamically<Function>用于指定在此模块评估期间调用import()时应如何加载模块。此选项是实验性模块 API 的一部分。我们不建议在生产环境中使用它。详细信息请参阅编译 API 对动态import()的支持。
创建新的 SourceTextModule 实例。
分配给 import.meta 对象的属性如果是对象,可能会允许模块访问指定 context 之外的信息。请使用 vm.runInContext() 在特定上下文中创建对象。
import vm from 'node:vm'; const contextifiedObject = vm.createContext({ secret: 42 }); const module = new vm.SourceTextModule( 'Object.getPrototypeOf(import.meta.prop).secret = secret;', { initializeImportMeta(meta) { // Note: this object is created in the top context. As such, // Object.getPrototypeOf(import.meta.prop) points to the // Object.prototype in the top context rather than that in // the contextified object. meta.prop = {}; }, }); // The module has an empty `moduleRequests` array. module.linkRequests([]); module.instantiate(); await module.evaluate(); // Now, Object.prototype.secret will be equal to 42. // // To fix this problem, replace // meta.prop = {}; // above with // meta.prop = vm.runInContext('{}', contextifiedObject);const vm = require('node:vm'); const contextifiedObject = vm.createContext({ secret: 42 }); (async () => { const module = new vm.SourceTextModule( 'Object.getPrototypeOf(import.meta.prop).secret = secret;', { initializeImportMeta(meta) { // Note: this object is created in the top context. As such, // Object.getPrototypeOf(import.meta.prop) points to the // Object.prototype in the top context rather than that in // the contextified object. meta.prop = {}; }, }); // The module has an empty `moduleRequests` array. module.linkRequests([]); module.instantiate(); await module.evaluate(); // Now, Object.prototype.secret will be equal to 42. // // To fix this problem, replace // meta.prop = {}; // above with // meta.prop = vm.runInContext('{}', contextifiedObject); })();
sourceTextModule.createCachedData()#
- 返回:
<Buffer>
创建一个可用于 SourceTextModule 构造函数 cachedData 选项的代码缓存。返回一个 Buffer。此方法可以在模块被评估前的任何时间、任意次数调用。
SourceTextModule 的代码缓存不包含任何 JavaScript 可观察状态。代码缓存可以安全地与脚本源代码一起保存,并用于多次构建新的 SourceTextModule 实例。
SourceTextModule 源码中的函数可以被标记为延迟编译,它们在 SourceTextModule 构建时不会被编译。这些函数将在首次调用时被编译。代码缓存序列化了 V8 当前已知的关于该 SourceTextModule 的元数据,可用于加速未来的编译。
// Create an initial module
const module = new vm.SourceTextModule('const a = 1;');
// Create cached data from this module
const cachedData = module.createCachedData();
// Create a new module using the cached data. The code must be the same.
const module2 = new vm.SourceTextModule('const a = 1;', { cachedData });
sourceTextModule.dependencySpecifiers#
稳定性:0 - 已弃用:请改为使用 sourceTextModule.moduleRequests。
- 类型:
<string[]>
此模块所有依赖项的说明符。返回的数组已被冻结,不允许对其进行任何更改。
对应于 ECMAScript 规范中循环模块记录(Cyclic Module Record)的 [[RequestedModules]] 字段。
sourceTextModule.hasAsyncGraph()#
- 返回:
<boolean>
遍历依赖关系图,如果其依赖项中的任何模块或该模块本身包含顶级 await 表达式,则返回 true,否则返回 false。
如果图足够大,搜索速度可能会很慢。
这要求模块首先被实例化。如果模块尚未实例化,将抛出错误。
sourceTextModule.hasTopLevelAwait()#
- 返回:
<boolean>
返回模块本身是否包含任何顶级 await 表达式。
这对应于 ECMAScript 规范中循环模块记录(Cyclic Module Record)中的 [[HasTLA]] 字段。
sourceTextModule.instantiate()#
- 返回:
<undefined>
使用链接的请求模块实例化模块。
这会解析模块的导入绑定,包括重新导出的绑定名称。当有任何绑定无法解析时,将同步抛出错误。
如果请求的模块包含循环依赖项,则必须在调用此方法之前对周期中的所有模块调用 sourceTextModule.linkRequests(modules) 方法。
sourceTextModule.linkRequests(modules)#
modules<vm.Module[]>此模块所依赖的vm.Module对象数组。数组中模块的顺序是sourceTextModule.moduleRequests的顺序。- 返回:
<undefined>
链接模块依赖项。必须在评估前调用此方法,且每个模块只能调用一次。
modules 数组中模块实例的顺序应对应于 sourceTextModule.moduleRequests 被解析的顺序。如果两个模块请求具有相同的说明符和导入属性,则必须使用相同的模块实例来解析它们,否则将抛出 ERR_MODULE_LINK_MISMATCH。例如,当链接此模块的请求时:
import foo from 'foo';
import source Foo from 'foo';
modules 数组必须包含对同一实例的两个引用,因为这两个模块请求相同但处于两个阶段。
如果模块没有依赖项,modules 数组可以为空。
用户可以使用 sourceTextModule.moduleRequests 来实现 ECMAScript 规范中宿主定义的 HostLoadImportedModule 抽象操作,并使用 sourceTextModule.linkRequests() 来对所有依赖项的模块批量调用规范定义的 FinishLoadingImportedModule。
决定依赖项的解析是同步还是异步取决于 SourceTextModule 的创建者。
在 modules 数组中的每个模块链接后,调用 sourceTextModule.instantiate()。
sourceTextModule.moduleRequests#
- 类型:
<ModuleRequest[]>此模块的依赖项。
此模块请求的导入依赖项。返回的数组已被冻结,不允许对其进行任何更改。
例如,给定一段源代码:
import foo from 'foo';
import fooAlias from 'foo';
import bar from './bar.js';
import withAttrs from '../with-attrs.ts' with { arbitraryAttr: 'attr-val' };
import source Module from 'wasm-mod.wasm';
sourceTextModule.moduleRequests 的值将为:
[
{
specifier: 'foo',
attributes: {},
phase: 'evaluation',
},
{
specifier: 'foo',
attributes: {},
phase: 'evaluation',
},
{
specifier: './bar.js',
attributes: {},
phase: 'evaluation',
},
{
specifier: '../with-attrs.ts',
attributes: { arbitraryAttr: 'attr-val' },
phase: 'evaluation',
},
{
specifier: 'wasm-mod.wasm',
attributes: {},
phase: 'source',
},
];
类:vm.SyntheticModule#
稳定性:1 - 实验性
此功能仅在启用了 --experimental-vm-modules 命令标志时可用。
- 继承自:
<vm.Module>
vm.SyntheticModule 类提供 WebIDL 规范中定义的合成模块记录(Synthetic Module Record)。合成模块的目的是提供一个通用接口,将非 JavaScript 资源暴露给 ECMAScript 模块图。
import { SyntheticModule } from 'node:vm'; const source = '{ "a": 1 }'; const syntheticModule = new SyntheticModule(['default'], function() { const obj = JSON.parse(source); this.setExport('default', obj); }); // Use `syntheticModule` in linking (async () => { await syntheticModule.link(() => {}); await syntheticModule.evaluate(); console.log('Default export:', syntheticModule.namespace.default); })();const { SyntheticModule } = require('node:vm'); const source = '{ "a": 1 }'; const syntheticModule = new SyntheticModule(['default'], function() { const obj = JSON.parse(source); this.setExport('default', obj); }); // Use `syntheticModule` in linking (async () => { await syntheticModule.link(() => {}); await syntheticModule.evaluate(); console.log('Default export:', syntheticModule.namespace.default); })();
new vm.SyntheticModule(exportNames, evaluateCallback[, options])#
exportNames<string[]>将从模块导出的名称数组。evaluateCallback<Function>模块评估时调用。options
创建新的 SyntheticModule 实例。
分配给此实例导出的对象可能会允许模块的导入者访问指定 context 之外的信息。请使用 vm.runInContext() 在特定上下文中创建对象。
syntheticModule.setExport(name, value)#
此方法使用给定值设置模块导出绑定槽。
import vm from 'node:vm'; const m = new vm.SyntheticModule(['x'], () => { m.setExport('x', 1); }); await m.evaluate(); assert.strictEqual(m.namespace.x, 1);const vm = require('node:vm'); (async () => { const m = new vm.SyntheticModule(['x'], () => { m.setExport('x', 1); }); await m.evaluate(); assert.strictEqual(m.namespace.x, 1); })();
类型:ModuleRequest#
- 类型:
<Object>specifier<string>所请求模块的说明符。attributes<Object>传递给 ImportDeclaration 中 WithClause 的"with"值;如果没有提供值,则为空对象。phase<string>所请求模块的阶段 ("source"或"evaluation")。
ModuleRequest 表示带有给定导入属性和阶段的模块导入请求。
vm.compileFunction(code[, params[, options]])#
code<string>要编译的函数主体。params<string[]>包含函数所有参数的字符串数组。options<Object>filename<string>指定此脚本生成的堆栈跟踪中使用的文件名。默认值:''。lineOffset<number>指定此脚本生成的堆栈跟踪中显示的行号偏移量。默认值:0。columnOffset<number>指定此脚本生成的堆栈跟踪中显示的第一行列号偏移量。默认值:0。cachedData<Buffer>|<TypedArray>|<DataView>为提供的源提供可选的Buffer、TypedArray或DataView,其中包含 V8 的代码缓存数据。这必须由先前使用相同code和params的vm.compileFunction()调用生成。produceCachedData<boolean>指定是否生成新的缓存数据。默认值:false。parsingContext<Object>应在其中编译所述函数的已上下文化的对象。contextExtensions<Object[]>一个包含编译时要应用的一组上下文扩展(包装当前作用域的对象)的数组。默认值:[]。importModuleDynamically<Function>|<vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER>用于指定在此函数评估期间调用import()时应如何加载模块。此选项是实验性模块 API 的一部分。我们不建议在生产环境中使用它。详细信息请参阅编译 API 对动态import()的支持。
- 返回:
<Function>
将给定的代码编译到提供的上下文中(如果未提供上下文,则使用当前上下文),并将其包装在具有给定 params 的函数中返回。
vm.constants#
- 类型:
<Object>
返回包含 VM 操作常用常量对象。
vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER#
稳定性:1.1 - 活跃开发中
一个常量,可用作 vm.Script 和 vm.compileFunction() 的 importModuleDynamically 选项,以便 Node.js 使用来自主上下文的默认 ESM 加载器来加载请求的模块。
详细信息请参阅编译 API 对动态 import() 的支持。
vm.createContext([contextObject[, options]])#
contextObject<Object>|<vm.constants.DONT_CONTEXTIFY>|<undefined>要么是vm.constants.DONT_CONTEXTIFY,要么是一个将被上下文化的对象。如果为undefined,将创建一个空的已上下文化对象以实现向后兼容。options<Object>name<string>新创建上下文的可读名称。默认值:'VM Context i',其中i是创建上下文的递增数字索引。origin<string>用于显示目的、与新创建上下文相对应的源(Origin)。源应格式化为 URL,但仅包含方案、主机和端口(如有必要),如URL对象的url.origin属性值。最重要的是,此字符串应省略尾随斜杠,因为它表示路径。默认值:''。codeGeneration<Object>microtaskMode<string>如果设为afterEvaluate,微任务(通过Promise和async function调度的任务)将在通过script.runInContext()运行脚本后立即运行。在这种情况下,它们会被包含在timeout和breakOnSigint的作用域中。importModuleDynamically<Function>|<vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER>用于指定当在此上下文中调用import()(且没有引用者脚本或模块时)应如何加载模块。此选项是实验性模块 API 的一部分。我们不建议在生产环境中使用它。详细信息请参阅编译 API 对动态import()的支持。
- 返回:
<Object>已上下文化的对象。
如果给定的 contextObject 是一个对象,vm.createContext() 方法将准备该对象并返回它的引用,以便可以在调用 vm.runInContext() 或 script.runInContext() 时使用它。在这样的脚本中,全局对象将被 contextObject 包装,保留其所有现有属性,同时也拥有任何标准全局对象所具有的内置对象和函数。在由 vm 模块运行的脚本之外,全局变量将保持不变。
import { createContext, runInContext } from 'node:vm'; global.globalVar = 3; const context = { globalVar: 1 }; createContext(context); runInContext('globalVar *= 2;', context); console.log(context); // Prints: { globalVar: 2 } console.log(global.globalVar); // Prints: 3const { createContext, runInContext } = require('node:vm'); global.globalVar = 3; const context = { globalVar: 1 }; createContext(context); runInContext('globalVar *= 2;', context); console.log(context); // Prints: { globalVar: 2 } console.log(global.globalVar); // Prints: 3
如果省略 contextObject(或显式传递为 undefined),将返回一个新的、空的已上下文化对象。
当新创建上下文中的全局对象被上下文化时,与普通全局对象相比,它有一些怪癖。例如,它不能被冻结。要创建一个没有上下文化怪癖的上下文,请将 vm.constants.DONT_CONTEXTIFY 作为 contextObject 参数传递。有关详细信息,请参阅 vm.constants.DONT_CONTEXTIFY 的文档。
vm.createContext() 方法主要用于创建一个可用于运行多个脚本的单个上下文。例如,如果模拟 Web 浏览器,该方法可用于创建一个代表窗口全局对象的单个上下文,然后在该上下文中一起运行所有 <script> 标签。
上下文提供的 name 和 origin 会通过 Inspector API 可见。
vm.isContext(object)#
如果给定的 object 已使用 vm.createContext() 进行上下文化,或者它是使用 vm.constants.DONT_CONTEXTIFY 创建的上下文的全局对象,则返回 true。
vm.measureMemory([options])#
稳定性:1 - 实验性
测量 V8 已知并由当前 V8 隔离(isolate)已知的所有上下文,或主上下文使用的内存。
options<Object>可选。- 返回:
<Promise>如果成功测量内存,Promise 将解析为一个包含内存使用信息对象。否则,它将被ERR_CONTEXT_NOT_INITIALIZED错误拒绝。
返回的 Promise 可能解析的对象格式特定于 V8 引擎,并且可能随着 V8 版本的变化而变化。
返回的结果与 v8.getHeapSpaceStatistics() 返回的统计数据不同,vm.measureMemory() 测量的是当前 V8 引擎实例中每个特定 V8 上下文可达的内存,而 v8.getHeapSpaceStatistics() 的结果测量的是当前 V8 实例中每个堆空间占用的内存。
import { createContext, measureMemory } from 'node:vm'; // Measure the memory used by the main context. measureMemory({ mode: 'summary' }) // This is the same as vm.measureMemory() .then((result) => { // The current format is: // { // total: { jsMemoryEstimate: 1601828, jsMemoryRange: [1601828, 5275288] }, // WebAssembly: { code: 0, metadata: 33962 }, // } console.log(result); }); const context = createContext({ a: 1 }); measureMemory({ mode: 'detailed', execution: 'eager' }).then((result) => { // Reference the context here so that it won't be GC'ed // until the measurement is complete. console.log('Context:', context.a); // { // total: { jsMemoryEstimate: 1767100, jsMemoryRange: [1767100, 5440560] }, // WebAssembly: { code: 0, metadata: 33962 }, // current: { jsMemoryEstimate: 1601828, jsMemoryRange: [1601828, 5275288] }, // other: [{ jsMemoryEstimate: 165272, jsMemoryRange: [Array] }], // } console.log(result); });const { createContext, measureMemory } = require('node:vm'); // Measure the memory used by the main context. measureMemory({ mode: 'summary' }) // This is the same as vm.measureMemory() .then((result) => { // The current format is: // { // total: { jsMemoryEstimate: 1601828, jsMemoryRange: [1601828, 5275288] }, // WebAssembly: { code: 0, metadata: 33962 }, // } console.log(result); }); const context = createContext({ a: 1 }); measureMemory({ mode: 'detailed', execution: 'eager' }).then((result) => { // Reference the context here so that it won't be GC'ed // until the measurement is complete. console.log('Context:', context.a); // { // total: { jsMemoryEstimate: 1767100, jsMemoryRange: [1767100, 5440560] }, // WebAssembly: { code: 0, metadata: 33962 }, // current: { jsMemoryEstimate: 1601828, jsMemoryRange: [1601828, 5275288] }, // other: [{ jsMemoryEstimate: 165272, jsMemoryRange: [Array] }], // } console.log(result); });
vm.runInContext(code, contextifiedObject[, options])#
code<string>要编译和运行的 JavaScript 代码。contextifiedObject<Object>当code被编译和运行时,将被用作global的已上下文化对象。options<Object>|<string>filename<string>指定此脚本生成的堆栈跟踪中使用的文件名。默认值:'evalmachine.<anonymous>'。lineOffset<number>指定此脚本生成的堆栈跟踪中显示的行号偏移量。默认值:0。columnOffset<number>指定此脚本生成的堆栈跟踪中显示的第一行列号偏移量。默认值:0。displayErrors<boolean>当为true时,如果编译code时发生Error,则导致错误的这一行代码将被附加到堆栈跟踪中。默认值:true。timeout<integer>指定在终止执行前执行code的毫秒数。如果执行被终止,将抛出Error。该值必须是严格的正整数。breakOnSigint<boolean>如果为true,接收SIGINT(Ctrl+C) 将终止执行并抛出Error。通过process.on('SIGINT')附加的现有事件处理程序在脚本执行期间会被禁用,但在此之后会继续生效。默认值:false。cachedData<Buffer>|<TypedArray>|<DataView>为提供的源提供可选的Buffer、TypedArray或DataView,其中包含 V8 的代码缓存数据。importModuleDynamically<Function>|<vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER>用于指定在此脚本评估期间调用import()时应如何加载模块。此选项是实验性模块 API 的一部分。我们不建议在生产环境中使用它。详细信息请参阅编译 API 对动态import()的支持。
vm.runInContext() 方法编译 code,在 contextifiedObject 的上下文中运行它,然后返回结果。运行代码无法访问本地作用域。contextifiedObject 对象必须已先前使用 vm.createContext() 方法进行上下文化。
如果 options 是字符串,则指定文件名。
以下示例使用单个已上下文化的对象编译和执行不同的脚本:
import { createContext, runInContext } from 'node:vm'; const contextObject = { globalVar: 1 }; createContext(contextObject); for (let i = 0; i < 10; ++i) { runInContext('globalVar *= 2;', contextObject); } console.log(contextObject); // Prints: { globalVar: 1024 }const { createContext, runInContext } = require('node:vm'); const contextObject = { globalVar: 1 }; createContext(contextObject); for (let i = 0; i < 10; ++i) { runInContext('globalVar *= 2;', contextObject); } console.log(contextObject); // Prints: { globalVar: 1024 }
vm.runInNewContext(code[, contextObject[, options]])#
code<string>要编译和运行的 JavaScript 代码。contextObject<Object>|<vm.constants.DONT_CONTEXTIFY>|<undefined>要么是vm.constants.DONT_CONTEXTIFY,要么是一个将被上下文化的对象。如果为undefined,将创建一个空的已上下文化对象以实现向后兼容。options<Object>|<string>filename<string>指定此脚本生成的堆栈跟踪中使用的文件名。默认值:'evalmachine.<anonymous>'。lineOffset<number>指定此脚本生成的堆栈跟踪中显示的行号偏移量。默认值:0。columnOffset<number>指定此脚本生成的堆栈跟踪中显示的第一行列号偏移量。默认值:0。displayErrors<boolean>当为true时,如果编译code时发生Error,则导致错误的这一行代码将被附加到堆栈跟踪中。默认值:true。timeout<integer>指定在终止执行前执行code的毫秒数。如果执行被终止,将抛出Error。该值必须是严格的正整数。breakOnSigint<boolean>如果为true,接收SIGINT(Ctrl+C) 将终止执行并抛出Error。通过process.on('SIGINT')附加的现有事件处理程序在脚本执行期间会被禁用,但在此之后会继续生效。默认值:false。contextName<string>新创建上下文的可读名称。默认值:'VM Context i',其中i是创建上下文的递增数字索引。contextOrigin<string>用于显示目的、与新创建上下文相对应的源(Origin)。源应格式化为 URL,但仅包含方案、主机和端口(如有必要),如URL对象的url.origin属性值。最重要的是,此字符串应省略尾随斜杠,因为它表示路径。默认值:''。contextCodeGeneration<Object>cachedData<Buffer>|<TypedArray>|<DataView>为提供的源提供可选的Buffer、TypedArray或DataView,其中包含 V8 的代码缓存数据。importModuleDynamically<Function>|<vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER>用于指定在此脚本评估期间调用import()时应如何加载模块。此选项是实验性模块 API 的一部分。我们不建议在生产环境中使用它。详细信息请参阅编译 API 对动态import()的支持。microtaskMode<string>如果设为afterEvaluate,微任务(通过Promise和async function调度的任务)将在脚本运行后立即运行。在这种情况下,它们会被包含在timeout和breakOnSigint的作用域中。
- 返回:
<any>脚本中执行的最后一条语句的结果。
此方法是 (new vm.Script(code, options)).runInContext(vm.createContext(options), options) 的快捷方式。如果 options 是字符串,则指定文件名。
它同时执行多项操作:
- 创建一个新上下文。
- 如果
contextObject是对象,则使用新上下文将其上下文化。如果contextObject未定义,则创建一个新对象并将其上下文化。如果contextObject是vm.constants.DONT_CONTEXTIFY,则不上下文化任何东西。 - 将代码编译为
vm.Script - 在创建的上下文中运行已编译的代码。代码无法访问调用此方法的作用域。
- 返回结果。
以下示例编译并执行递增全局变量并设置一个新变量的代码。这些全局变量包含在 contextObject 中。
import { runInNewContext, constants } from 'node:vm'; const contextObject = { animal: 'cat', count: 2, }; runInNewContext('count += 1; name = "kitty"', contextObject); console.log(contextObject); // Prints: { animal: 'cat', count: 3, name: 'kitty' } // This would throw if the context is created from a contextified object. // vm.constants.DONT_CONTEXTIFY allows creating contexts with ordinary global objects that // can be frozen. const frozenContext = runInNewContext( 'Object.freeze(globalThis); globalThis;', constants.DONT_CONTEXTIFY, );const { runInNewContext, constants } = require('node:vm'); const contextObject = { animal: 'cat', count: 2, }; runInNewContext('count += 1; name = "kitty"', contextObject); console.log(contextObject); // Prints: { animal: 'cat', count: 3, name: 'kitty' } // This would throw if the context is created from a contextified object. // vm.constants.DONT_CONTEXTIFY allows creating contexts with ordinary global objects that // can be frozen. const frozenContext = runInNewContext( 'Object.freeze(globalThis); globalThis;', constants.DONT_CONTEXTIFY, );
vm.runInThisContext(code[, options])#
code<string>要编译和运行的 JavaScript 代码。options<Object>|<string>filename<string>指定此脚本生成的堆栈跟踪中使用的文件名。默认值:'evalmachine.<anonymous>'。lineOffset<number>指定此脚本生成的堆栈跟踪中显示的行号偏移量。默认值:0。columnOffset<number>指定此脚本生成的堆栈跟踪中显示的第一行列号偏移量。默认值:0。displayErrors<boolean>当为true时,如果编译code时发生Error,则导致错误的这一行代码将被附加到堆栈跟踪中。默认值:true。timeout<integer>指定在终止执行前执行code的毫秒数。如果执行被终止,将抛出Error。该值必须是严格的正整数。breakOnSigint<boolean>如果为true,接收SIGINT(Ctrl+C) 将终止执行并抛出Error。通过process.on('SIGINT')附加的现有事件处理程序在脚本执行期间会被禁用,但在此之后会继续生效。默认值:false。cachedData<Buffer>|<TypedArray>|<DataView>为提供的源提供可选的Buffer、TypedArray或DataView,其中包含 V8 的代码缓存数据。importModuleDynamically<Function>|<vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER>用于指定在此脚本评估期间调用import()时应如何加载模块。此选项是实验性模块 API 的一部分。我们不建议在生产环境中使用它。详细信息请参阅编译 API 对动态import()的支持。
- 返回:
<any>脚本中执行的最后一条语句的结果。
vm.runInThisContext() 编译 code,在当前 global 的上下文中运行它,并返回结果。运行代码无法访问本地作用域,但可以访问当前 global 对象。
如果 options 是字符串,则指定文件名。
以下示例说明了使用 vm.runInThisContext() 和 JavaScript eval() 函数来运行相同的代码:
import { runInThisContext } from 'node:vm'; let localVar = 'initial value'; const vmResult = runInThisContext('localVar = "vm";'); console.log(`vmResult: '${vmResult}', localVar: '${localVar}'`); // Prints: vmResult: 'vm', localVar: 'initial value' const evalResult = eval('localVar = "eval";'); console.log(`evalResult: '${evalResult}', localVar: '${localVar}'`); // Prints: evalResult: 'eval', localVar: 'eval'const { runInThisContext } = require('node:vm'); let localVar = 'initial value'; const vmResult = runInThisContext('localVar = "vm";'); console.log(`vmResult: '${vmResult}', localVar: '${localVar}'`); // Prints: vmResult: 'vm', localVar: 'initial value' const evalResult = eval('localVar = "eval";'); console.log(`evalResult: '${evalResult}', localVar: '${localVar}'`); // Prints: evalResult: 'eval', localVar: 'eval'
因为 vm.runInThisContext() 无法访问本地作用域,所以 localVar 未被更改。相反,直接 eval() 调用确实可以访问本地作用域,因此 localVar 的值被更改了。通过这种方式,vm.runInThisContext() 非常类似于一个间接 eval() 调用,例如 (0,eval)('code')。
示例:在虚拟机(VM)内运行 HTTP 服务器#
当使用 script.runInThisContext() 或 vm.runInThisContext() 时,代码是在当前 V8 全局上下文中执行的。传递给此 VM 上下文的代码将拥有其自己的独立作用域。
为了使用 node:http 模块运行一个简单的 Web 服务器,传递给上下文的代码必须自行调用 require('node:http'),或者拥有一个传递给它的 node:http 模块引用。例如:
import { runInThisContext } from 'node:vm'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const code = ` ((require) => { const { createServer } = require('node:http'); createServer((request, response) => { response.writeHead(200, { 'Content-Type': 'text/plain' }); response.end('Hello World\\n'); }).listen(8124); console.log('Server running at http://127.0.0.1:8124/'); })`; runInThisContext(code)(require);const { runInThisContext } = require('node:vm'); const code = ` ((require) => { const { createServer } = require('node:http'); createServer((request, response) => { response.writeHead(200, { 'Content-Type': 'text/plain' }); response.end('Hello World\\n'); }).listen(8124); console.log('Server running at http://127.0.0.1:8124/'); })`; runInThisContext(code)(require);
上述情况下的 require() 与它所传递的上下文共享状态。当执行不可信代码时,这可能会带来风险,例如以不希望的方式更改上下文中的对象。
“上下文化”(contextify)一个对象是什么意思?#
所有在 Node.js 中执行的 JavaScript 都在一个“上下文”的作用域内运行。根据 V8 嵌入器指南:
在 V8 中,上下文是一个执行环境,允许独立的、不相关的 JavaScript 应用程序在单个 V8 实例中运行。您必须显式指定要在其中运行任何 JavaScript 代码的上下文。
当使用对象调用 vm.createContext() 方法时,contextObject 参数将用于包装 V8 上下文新实例的全局对象(如果 contextObject 为 undefined,则在上下文化之前会从当前上下文创建一个新对象)。此 V8 上下文为使用 node:vm 模块方法运行的 code 提供了一个独立的全局环境,在其中它可以运行。创建 V8 上下文并将其与外部上下文中的 contextObject 关联的过程,即本文档所称的“上下文化”对象。
上下文化会给上下文中的 globalThis 值引入一些怪癖。例如,它不能被冻结,并且与外部上下文中的 contextObject 在引用上不相等。
import { createContext, runInContext } from 'node:vm'; // An undefined `contextObject` option makes the global object contextified. const context = createContext(); console.log(runInContext('globalThis', context) === context); // false // A contextified global object cannot be frozen. try { runInContext('Object.freeze(globalThis);', context); } catch (e) { console.log(`${e.constructor.name}: ${e.message}`); // TypeError: Cannot freeze } console.log(runInContext('globalThis.foo = 1; foo;', context)); // 1const { createContext, runInContext } = require('node:vm'); // An undefined `contextObject` option makes the global object contextified. const context = createContext(); console.log(runInContext('globalThis', context) === context); // false // A contextified global object cannot be frozen. try { runInContext('Object.freeze(globalThis);', context); } catch (e) { console.log(`${e.constructor.name}: ${e.message}`); // TypeError: Cannot freeze } console.log(runInContext('globalThis.foo = 1; foo;', context)); // 1
要创建一个具有普通全局对象的上下文,并以较少的怪癖在外部上下文中访问全局代理,请指定 vm.constants.DONT_CONTEXTIFY 作为 contextObject 参数。
vm.constants.DONT_CONTEXTIFY#
当此常量在 VM API 中用作 contextObject 参数时,它指示 Node.js 创建一个上下文,而不以特定于 Node.js 的方式用另一个对象包装其全局对象。因此,新上下文中的 globalThis 值将表现得更接近于普通值。
import { createContext, runInContext, constants } from 'node:vm'; // Use vm.constants.DONT_CONTEXTIFY to freeze the global object. const context = createContext(constants.DONT_CONTEXTIFY); runInContext('Object.freeze(globalThis);', context); try { runInContext('bar = 1; bar;', context); } catch (e) { console.log(`${e.constructor.name}: ${e.message}`); // ReferenceError: bar is not defined }const { createContext, runInContext, constants } = require('node:vm'); // Use vm.constants.DONT_CONTEXTIFY to freeze the global object. const context = createContext(constants.DONT_CONTEXTIFY); runInContext('Object.freeze(globalThis);', context); try { runInContext('bar = 1; bar;', context); } catch (e) { console.log(`${e.constructor.name}: ${e.message}`); // ReferenceError: bar is not defined }
当 vm.constants.DONT_CONTEXTIFY 用作 vm.createContext() 的 contextObject 参数时,返回的对象是一个对新创建上下文中全局对象的代理类对象,具有较少的 Node.js 特定怪癖。它与新上下文中的 globalThis 值在引用上相等,可以从上下文外部修改,并且可以直接用于访问新上下文中的内置对象。
import { createContext, runInContext, constants } from 'node:vm'; const context = createContext(constants.DONT_CONTEXTIFY); // Returned object is reference equal to globalThis in the new context. console.log(runInContext('globalThis', context) === context); // true // Can be used to access globals in the new context directly. console.log(context.Array); // [Function: Array] runInContext('foo = 1;', context); console.log(context.foo); // 1 context.bar = 1; console.log(runInContext('bar;', context)); // 1 // Can be frozen and it affects the inner context. Object.freeze(context); try { runInContext('baz = 1; baz;', context); } catch (e) { console.log(`${e.constructor.name}: ${e.message}`); // ReferenceError: baz is not defined }const { createContext, runInContext, constants } = require('node:vm'); const context = createContext(constants.DONT_CONTEXTIFY); // Returned object is reference equal to globalThis in the new context. console.log(runInContext('globalThis', context) === context); // true // Can be used to access globals in the new context directly. console.log(context.Array); // [Function: Array] runInContext('foo = 1;', context); console.log(context.foo); // 1 context.bar = 1; console.log(runInContext('bar;', context)); // 1 // Can be frozen and it affects the inner context. Object.freeze(context); try { runInContext('baz = 1; baz;', context); } catch (e) { console.log(`${e.constructor.name}: ${e.message}`); // ReferenceError: baz is not defined }
超时与异步任务及 Promise 的交互#
Promise 和 async function 可以异步调度由 JavaScript 引擎运行的任务。默认情况下,这些任务在当前堆栈上的所有 JavaScript 函数执行完成后运行。这允许绕过 timeout 和 breakOnSigint 选项的功能。
例如,以下代码由 vm.runInNewContext() 以 5 毫秒的超时时间执行,它调度了一个在 promise 解析后运行的无限循环。调度的循环永远不会被超时中断:
import { runInNewContext } from 'node:vm'; function loop() { console.log('entering loop'); while (1) console.log(Date.now()); } runInNewContext( 'Promise.resolve().then(() => loop());', { loop, console }, { timeout: 5 }, ); // This is printed *before* 'entering infinite loop' (!) console.log('done executing');const { runInNewContext } = require('node:vm'); function loop() { console.log('entering loop'); while (1) console.log(Date.now()); } runInNewContext( 'Promise.resolve().then(() => loop());', { loop, console }, { timeout: 5 }, ); // This is printed *before* 'entering infinite loop' (!) console.log('done executing');
这可以通过向创建 Context 的代码传递 microtaskMode: 'afterEvaluate' 来解决:
import { runInNewContext } from 'node:vm'; function loop() { while (1) console.log(Date.now()); } runInNewContext( 'Promise.resolve().then(() => loop());', { loop, console }, { timeout: 5, microtaskMode: 'afterEvaluate' }, );const { runInNewContext } = require('node:vm'); function loop() { while (1) console.log(Date.now()); } runInNewContext( 'Promise.resolve().then(() => loop());', { loop, console }, { timeout: 5, microtaskMode: 'afterEvaluate' }, );
在这种情况下,通过 promise.then() 调度的微任务将在从 vm.runInNewContext() 返回之前运行,并将被 timeout 功能中断。这仅适用于在 vm.Context 中运行的代码,因此例如 vm.runInThisContext() 不接受此选项。
Promise 回调被输入到它们创建时所在上下文的微任务队列中。例如,如果上面示例中的 () => loop() 仅替换为 loop,则 loop 将被推送到全局微任务队列中,因为它是来自外部(主)上下文的函数,因此它也能够绕过超时。
如果在 vm.Context 中提供了异步调度函数(如 process.nextTick()、queueMicrotask()、setTimeout()、setImmediate() 等),传递给它们的函数将被添加到所有上下文共享的全局队列中。因此,传递给这些函数的回调也无法通过超时控制。
当 microtaskMode 为 'afterEvaluate' 时,需注意在上下文间共享 Promise#
在 'afterEvaluate' 模式下,Context 拥有自己的微任务队列,与外部(主)上下文使用的全局微任务队列分开。虽然此模式对于强制执行 timeout 和启用异步任务的 breakOnSigint 是必要的,但它也使得在上下文间共享 Promise 变得具有挑战性。
在下面的示例中,一个 Promise 在内部上下文中创建并与外部上下文共享。当外部上下文 await 该 Promise 时,外部上下文的执行流以令人惊讶的方式被中断:日志语句永远不会被执行。
import { createContext, runInContext } from 'node:vm'; const inner_context = createContext({}, { microtaskMode: 'afterEvaluate' }); // runInContext() returns a Promise created in the inner context. const inner_promise = runInContext('Promise.resolve()', inner_context); // As part of performing `await`, the JavaScript runtime must enqueue a task // on the microtask queue of the context where `inner_promise` was created. // A task is added on the inner microtask queue, but **it will not be run // automatically**: this task will remain pending indefinitely. // // Since the outer microtask queue is empty, execution in the outer module // falls through, and the log statement below is never executed. await inner_promise; console.log('this will NOT be printed');const { createContext, runInContext } = require('node:vm'); // runInContext() returns a Promise created in the inner context. const inner_context = createContext({}, { microtaskMode: 'afterEvaluate' }); (async () => { const inner_promise = runInContext('Promise.resolve()', inner_context); // As part of performing `await`, the JavaScript runtime must enqueue a task // on the microtask queue of the context where `inner_promise` was created. // A task is added on the inner microtask queue, but **it will not be run // automatically**: this task will remain pending indefinitely. // // Since the outer microtask queue is empty, execution in the outer module // falls through, and the log statement below is never executed. await inner_promise; console.log('this will NOT be printed'); })();
要成功地在具有不同微任务队列的上下文之间共享 Promise,有必要确保无论何时外部上下文在内部微任务队列上调度任务,内部微任务队列上的任务都将运行。
只要在脚本或模块上调用 runInContext() 或 SourceTextModule.evaluate() 时,给定上下文的微任务队列上的任务就会运行。在我们的示例中,可以通过在 await inner_promise 之前调度第二次 runInContext() 调用来恢复正常的执行流。
// Schedule `runInContext()` to manually drain the inner context microtask
// queue; it will run after the `await` statement below.
setImmediate(() => {
vm.runInContext('', context);
});
await inner_promise;
console.log('OK');
注意: 严格来说,在这种模式下,node:vm 背离了 ECMAScript 规范中关于排队作业(enqueing jobs)的文字规定,允许来自不同上下文的异步任务以不同于它们被排队的顺序运行。
编译 API 对动态 import() 的支持#
以下 API 支持 importModuleDynamically 选项,以在由 vm 模块编译的代码中启用动态 import()。
new vm.Scriptvm.compileFunction()new vm.SourceTextModulevm.runInThisContext()vm.runInContext()vm.runInNewContext()vm.createContext()
此选项仍是实验性模块 API 的一部分。我们不建议在生产环境中使用它。
当未指定 importModuleDynamically 选项或其值为 undefined 时#
如果未指定此选项,或者其值为 undefined,包含 import() 的代码仍然可以由 vm API 编译,但当编译后的代码执行并实际调用 import() 时,结果将因 ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING 而拒绝。
当 importModuleDynamically 为 vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER 时#
此选项目前不支持 vm.SourceTextModule。
使用此选项,当在编译代码中发起 import() 时,Node.js 将使用来自主上下文的默认 ESM 加载器来加载所请求的模块,并将其返回给正在执行的代码。
这使得编译后的代码能够访问 Node.js 内置模块(如 fs 或 http)。如果代码是在不同的上下文中执行的,请注意由从主上下文加载的模块创建的对象仍然来自主上下文,且不是新上下文中内置类的 instanceof 实例。
const { Script, constants } = require('node:vm'); const script = new Script( 'import("node:fs").then(({readFile}) => readFile instanceof Function)', { importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER }); // false: URL loaded from the main context is not an instance of the Function // class in the new context. script.runInNewContext().then(console.log);import { Script, constants } from 'node:vm'; const script = new Script( 'import("node:fs").then(({readFile}) => readFile instanceof Function)', { importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER }); // false: URL loaded from the main context is not an instance of the Function // class in the new context. script.runInNewContext().then(console.log);
此选项还允许脚本或函数加载用户模块。
import { Script, constants } from 'node:vm'; import { resolve } from 'node:path'; import { writeFileSync } from 'node:fs'; // Write test.js and test.txt to the directory where the current script // being run is located. writeFileSync(resolve(import.meta.dirname, 'test.mjs'), 'export const filename = "./test.json";'); writeFileSync(resolve(import.meta.dirname, 'test.json'), '{"hello": "world"}'); // Compile a script that loads test.mjs and then test.json // as if the script is placed in the same directory. const script = new Script( `(async function() { const { filename } = await import('./test.mjs'); return import(filename, { with: { type: 'json' } }) })();`, { filename: resolve(import.meta.dirname, 'test-with-default.js'), importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER, }); // { default: { hello: 'world' } } script.runInThisContext().then(console.log);const { Script, constants } = require('node:vm'); const { resolve } = require('node:path'); const { writeFileSync } = require('node:fs'); // Write test.js and test.txt to the directory where the current script // being run is located. writeFileSync(resolve(__dirname, 'test.mjs'), 'export const filename = "./test.json";'); writeFileSync(resolve(__dirname, 'test.json'), '{"hello": "world"}'); // Compile a script that loads test.mjs and then test.json // as if the script is placed in the same directory. const script = new Script( `(async function() { const { filename } = await import('./test.mjs'); return import(filename, { with: { type: 'json' } }) })();`, { filename: resolve(__dirname, 'test-with-default.js'), importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER, }); // { default: { hello: 'world' } } script.runInThisContext().then(console.log);
使用来自主上下文的默认加载器加载用户模块有几个注意事项:
- 正在解析的模块将相对于传递给
vm.Script或vm.compileFunction()的filename选项。解析可以适用于作为绝对路径或 URL 字符串的filename。如果filename是一个既不是绝对路径也不是 URL 的字符串,或者它是 undefined,解析将相对于进程的当前工作目录。对于vm.createContext(),解析始终相对于当前工作目录,因为此选项仅在没有引用者脚本或模块时使用。 - 对于解析为特定路径的任何给定
filename,一旦进程成功从该路径加载特定模块,结果可能会被缓存,并且后续从同一路径加载同一模块将返回相同的内容。如果filename是 URL 字符串,如果它具有不同的搜索参数,则不会命中缓存。对于非 URL 字符串的filename,目前没有办法绕过缓存行为。
当 importModuleDynamically 为函数时#
当 importModuleDynamically 为函数时,它将在编译代码中调用 import() 时被调用,供用户自定义如何编译和评估请求的模块。目前,Node.js 实例必须在启动时添加 --experimental-vm-modules 标志才能使此选项工作。如果未设置该标志,此回调将被忽略。如果评估的代码确实调用了 import(),结果将因 ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG 而拒绝。
回调 importModuleDynamically(specifier, referrer, importAttributes) 具有以下签名:
specifier<string>传递给import()的说明符referrer<vm.Script>|<Function>|<vm.SourceTextModule>|<Object>对于new vm.Script、vm.runInThisContext、vm.runInContext和vm.runInNewContext,referrer 是已编译的vm.Script。对于vm.compileFunction,它是已编译的Function;对于new vm.SourceTextModule,它是已编译的vm.SourceTextModule;对于vm.createContext(),它是上下文Object。importAttributes<Object>传递给optionsExpression可选参数的"with"值,如果未提供值,则为空对象。phase<string>动态导入的阶段("source"或"evaluation")。- 返回:
<Module Namespace Object>|<vm.Module>建议返回vm.Module,以便利用错误跟踪功能,并避免包含then函数导出的命名空间所带来的问题。
// This script must be run with --experimental-vm-modules. import { Script, SyntheticModule } from 'node:vm'; const script = new Script('import("foo.json", { with: { type: "json" } })', { async importModuleDynamically(specifier, referrer, importAttributes) { console.log(specifier); // 'foo.json' console.log(referrer); // The compiled script console.log(importAttributes); // { type: 'json' } const m = new SyntheticModule(['bar'], () => { }); await m.link(() => { }); m.setExport('bar', { hello: 'world' }); return m; }, }); const result = await script.runInThisContext(); console.log(result); // { bar: { hello: 'world' } }// This script must be run with --experimental-vm-modules. const { Script, SyntheticModule } = require('node:vm'); (async function main() { const script = new Script('import("foo.json", { with: { type: "json" } })', { async importModuleDynamically(specifier, referrer, importAttributes) { console.log(specifier); // 'foo.json' console.log(referrer); // The compiled script console.log(importAttributes); // { type: 'json' } const m = new SyntheticModule(['bar'], () => { }); await m.link(() => { }); m.setExport('bar', { hello: 'world' }); return m; }, }); const result = await script.runInThisContext(); console.log(result); // { bar: { hello: 'world' } } })();