VM (执行 JavaScript)#

稳定性:2 - 稳定

源代码: lib/vm.js

node:vm 模块支持在 V8 虚拟机上下文中编译和运行代码。

node:vm 模块不是一个安全机制。不要用它来运行不受信任的代码。

JavaScript 代码可以被编译并立即运行,或者编译、保存,并在稍后运行。

一个常见的用例是在不同的 V8 上下文中运行代码。这意味着被调用的代码具有与调用代码不同的全局对象。

可以通过情境化(contextifying)一个对象来提供上下文。被调用的代码会将上下文中的任何属性视为全局变量。由被调用代码引起的全局变量的任何更改都会反映在上下文对象中。

const vm = require('node:vm');

const x = 1;

const context = { x: 2 };
vm.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.
vm.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> 提供一个可选的 BufferTypedArrayDataView,其中包含所提供源码的 V8 代码缓存数据。提供后,cachedDataRejected 的值将根据 V8 是否接受数据而被设置为 truefalse
    • produceCachedData <boolean> 当为 true 且不存在 cachedData 时,V8 将尝试为 code 生成代码缓存数据。成功后,将生成一个包含 V8 代码缓存数据的 Buffer,并存储在返回的 vm.Script 实例的 cachedData 属性中。cachedDataProduced 的值将根据代码缓存数据是否成功生成而被设置为 truefalse。此选项已弃用,推荐使用 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#

当创建 vm.Script 时提供了 cachedData,此值将根据 V8 是否接受数据而被设置为 truefalse。否则,值为 undefined

script.createCachedData()#

创建一个可用于 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>
    • displayErrors <boolean> 当为 true 时,如果在编译 code 时发生 Error,导致错误的代码行会附加到堆栈跟踪中。默认值: true
    • timeout <integer> 指定在终止执行前执行 code 的毫秒数。如果执行被终止,将抛出一个 Error。该值必须是严格的正整数。
    • breakOnSigint <boolean> 如果为 true,接收到 SIGINT (Ctrl+C) 将终止执行并抛出一个 Error。通过 process.on('SIGINT') 附加的现有事件处理器在脚本执行期间被禁用,但在之后会继续工作。默认值: false
  • 返回:<any> 脚本中执行的最后一个语句的结果。

在给定的 contextifiedObject 中运行由 vm.Script 对象包含的已编译代码,并返回结果。运行中的代码无法访问局部作用域。

以下示例编译了增加一个全局变量、设置另一个全局变量值的代码,然后多次执行该代码。全局变量包含在 context 对象中。

const vm = require('node:vm');

const context = {
  animal: 'cat',
  count: 2,
};

const script = new vm.Script('count += 1; name = "kitty";');

vm.createContext(context);
for (let i = 0; i < 10; ++i) {
  script.runInContext(context);
}

console.log(context);
// Prints: { animal: 'cat', count: 12, name: 'kitty' } 

使用 timeoutbreakOnSigint 选项将导致启动新的事件循环和相应的线程,这会带来不可忽略的性能开销。

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>
      • strings <boolean> 如果设置为 false,任何对 eval 或函数构造函数(Function, GeneratorFunction 等)的调用都将抛出 EvalError默认值: true
      • wasm <boolean> 如果设置为 false,任何编译 WebAssembly 模块的尝试都将抛出 WebAssembly.CompileError默认值: true
    • microtaskMode <string> 如果设置为 afterEvaluate,微任务(通过 Promiseasync function 调度的任务)将在脚本运行后立即运行。在这种情况下,它们被包含在 timeoutbreakOnSigint 的作用域内。
  • 返回:<any> 脚本中执行的最后一个语句的结果。

此方法是 script.runInContext(vm.createContext(options), options) 的快捷方式。它一次完成多项工作:

  1. 创建一个新的上下文。
  2. 如果 contextObject 是一个对象,则用新上下文将其情境化。如果 contextObject 是 undefined,则创建一个新对象并将其情境化。如果 contextObjectvm.constants.DONT_CONTEXTIFY,则不情境化任何东西。
  3. 在创建的上下文中运行由 vm.Script 对象包含的已编译代码。代码无法访问调用此方法的作用域。
  4. 返回结果。

以下示例编译了设置全局变量的代码,然后在不同的上下文中多次执行该代码。全局变量在每个单独的 context 中设置和包含。

const vm = require('node:vm');

const script = new vm.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.
// vm.constants.DONT_CONTEXTIFY allows creating contexts with ordinary
// global objects that can be frozen.
const freezeScript = new vm.Script('Object.freeze(globalThis); globalThis;');
const frozenContext = freezeScript.runInNewContext(vm.constants.DONT_CONTEXTIFY); 

script.runInThisContext([options])#

  • options <Object>
    • displayErrors <boolean> 当为 true 时,如果在编译 code 时发生 Error,导致错误的代码行会附加到堆栈跟踪中。默认值: true
    • timeout <integer> 指定在终止执行前执行 code 的毫秒数。如果执行被终止,将抛出一个 Error。该值必须是严格的正整数。
    • breakOnSigint <boolean> 如果为 true,接收到 SIGINT (Ctrl+C) 将终止执行并抛出一个 Error。通过 process.on('SIGINT') 附加的现有事件处理器在脚本执行期间被禁用,但在之后会继续工作。默认值: false
  • 返回:<any> 脚本中执行的最后一个语句的结果。

在当前 global 对象的上下文中运行由 vm.Script 包含的已编译代码。运行中的代码无法访问局部作用域,但可以访问当前的 global 对象。

以下示例编译了增加一个 global 变量的代码,然后多次执行该代码:

const vm = require('node:vm');

global.globalVar = 0;

const script = new vm.Script('globalVar += 1', { filename: 'myfile.vm' });

for (let i = 0; i < 1000; ++i) {
  script.runInThisContext();
}

console.log(globalVar);

// 1000 

script.sourceMapURL#

当脚本从包含 source map 魔术注释的源编译时,此属性将被设置为 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 规范中定义的模块记录

然而,与 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: referencingModule.context });
      moduleMap.set(specifier, linkedModule);
      // 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: referencingModule.context });
        moduleMap.set(specifier, linkedModule);
        // 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#

如果 module.status'errored',此属性包含模块在评估期间抛出的异常。如果状态是其他任何值,访问此属性将导致抛出异常。

由于可能与 throw undefined; 产生歧义,不能使用 undefined 值来表示没有抛出异常的情况。

对应于 ECMAScript 规范中循环模块记录[[EvaluationError]] 字段。

module.evaluate([options])#

  • options <Object>
    • timeout <integer> 指定在终止执行前评估的毫秒数。如果执行被中断,将抛出一个 Error。该值必须是严格的正整数。
    • breakOnSigint <boolean> 如果为 true,接收到 SIGINT (Ctrl+C) 将终止执行并抛出一个 Error。通过 process.on('SIGINT') 附加的现有事件处理器在脚本执行期间被禁用,但在之后会继续工作。默认值: false
  • 返回:<Promise> 成功时兑现为 undefined

评估该模块。

这必须在模块链接后调用;否则它将拒绝。当模块已经被评估时也可以调用它,在这种情况下,如果初始评估成功(module.status'evaluated'),它将什么也不做,或者如果初始评估导致异常(module.status'errored'),它将重新抛出该异常。

当模块正在评估时(module.status'evaluating'),不能调用此方法。

对应于 ECMAScript 规范中循环模块记录Evaluate() 具体方法字段。

module.identifier#

当前模块的标识符,如构造函数中所设置。

module.link(linker)#

  • linker <Function>
    • specifier <string> 所请求模块的说明符

      import foo from 'foo';
      //              ^^^^^ the module specifier 
    • referencingModule <vm.Module> 调用 link()Module 对象。

    • extra <Object>

      • attributes <Object> 来自属性的数据
        import foo from 'foo' with { name: 'value' };
        //                         ^^^^^^^^^^^^^^^^^ the attribute 
        根据 ECMA-262,如果存在不支持的属性,宿主环境应触发错误。
      • assert <Object> extra.attributes 的别名。
    • 返回:<vm.Module> | <Promise>

  • 返回:<Promise>

链接模块依赖。此方法必须在评估前调用,并且每个模块只能调用一次。

使用 sourceTextModule.linkRequests(modules)sourceTextModule.instantiate() 来同步或异步地链接模块。

该函数应返回一个 Module 对象或一个最终解析为 Module 对象的 Promise。返回的 Module 必须满足以下两个不变式:

  • 它必须属于与父 Module 相同的上下文。
  • 它的 status 不能是 'errored'

如果返回的 Modulestatus'unlinked',此方法将使用相同的 linker 函数递归地在返回的 Module 上调用。

link() 返回一个 Promise,当所有链接实例都解析为有效的 Module 时,它将被兑现;如果链接器函数抛出异常或返回无效的 Module,它将被拒绝。

链接器函数大致对应于 ECMAScript 规范中由实现定义的 HostResolveImportedModule 抽象操作,但有几个关键区别:

在模块链接期间实际使用的 HostResolveImportedModule 实现是返回在链接期间链接的模块。由于那时所有模块都已完全链接,因此根据规范,HostResolveImportedModule 实现是完全同步的。

对应于 ECMAScript 规范中循环模块记录Link() 具体方法字段。

module.namespace#

模块的命名空间对象。这只在链接 (module.link()) 完成后可用。

对应于 ECMAScript 规范中的 GetModuleNamespace 抽象操作。

module.status#

模块的当前状态。将是以下之一:

  • 'unlinked': module.link() 尚未被调用。

  • 'linking': module.link() 已被调用,但链接器函数返回的所有 Promise 尚未被解析。

  • 'linked': 模块已成功链接,并且其所有依赖项都已链接,但 module.evaluate() 尚未被调用。

  • 'evaluating': 模块正在通过对其自身或父模块的 module.evaluate() 进行评估。

  • 'evaluated': 模块已成功评估。

  • 'errored': 模块已被评估,但抛出了一个异常。

除了 'errored',此状态字符串对应于规范中循环模块记录[[Status]] 字段。'errored' 对应于规范中的 'evaluated',但 [[EvaluationError]] 被设置为一个非 undefined 的值。

类:vm.SourceTextModule#

稳定性:1 - 实验性

此功能仅在启用 --experimental-vm-modules 命令行标志时可用。

vm.SourceTextModule 类提供了 ECMAScript 规范中定义的源文本模块记录

new vm.SourceTextModule(code[, options])#

  • code <string> 要解析的 JavaScript 模块代码
  • 选项
    • identifier <string> 用于堆栈跟踪的字符串。默认值: 'vm:module(i)',其中 i 是特定于上下文的递增索引。
    • cachedData <Buffer> | <TypedArray> | <DataView> 提供一个可选的 BufferTypedArrayDataView,其中包含所提供源码的 V8 代码缓存数据。code 必须与创建此 cachedData 的模块的 code 相同。
    • context <Object> 一个情境化的对象,由 vm.createContext() 方法返回,用于编译和评估此 Module。如果未指定上下文,模块将在当前执行上下文中评估。
    • lineOffset <integer> 指定此 Module 产生的堆栈跟踪中显示的行号偏移量。默认值: 0
    • columnOffset <integer> 指定此 Module 产生的堆栈跟踪中显示的首行列号偏移量。默认值: 0
    • initializeImportMeta <Function> 在评估此 Module 期间调用,以初始化 import.meta
    • 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()#

创建一个可用于 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#

此模块所有依赖项的说明符。返回的数组被冻结,不允许任何更改。

对应于 ECMAScript 规范中循环模块记录[[RequestedModules]] 字段。

sourceTextModule.hasAsyncGraph()#

遍历依赖关系图,如果其依赖项或模块本身中的任何模块包含顶层 await 表达式,则返回 true,否则返回 false

如果图足够大,搜索可能会很慢。

这需要模块首先被实例化。如果模块尚未实例化,将抛出错误。

sourceTextModule.hasTopLevelAwait()#

返回模块本身是否包含任何顶层 await 表达式。

这对应于 ECMAScript 规范中循环模块记录中的 [[HasTLA]] 字段。

sourceTextModule.instantiate()#

使用已链接的请求模块实例化该模块。

这将解析模块的导入绑定,包括重新导出的绑定名称。当有任何无法解析的绑定时,将同步抛出错误。

如果请求的模块包含循环依赖,必须在调用此方法之前,对循环中的所有模块调用 sourceTextModule.linkRequests(modules) 方法。

sourceTextModule.linkRequests(modules)#

链接模块依赖。此方法必须在评估前调用,并且每个模块只能调用一次。

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#

此模块请求的导入依赖项。返回的数组被冻结,以禁止任何更改。

例如,给定一个源文本

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.SyntheticModule 类提供了 WebIDL 规范中定义的合成模块记录。合成模块的目的是为将非 JavaScript 源暴露给 ECMAScript 模块图提供一个通用接口。

const vm = require('node:vm');

const source = '{ "a": 1 }';
const module = new vm.SyntheticModule(['default'], function() {
  const obj = JSON.parse(source);
  this.setExport('default', obj);
});

// Use `module` in linking... 

new vm.SyntheticModule(exportNames, evaluateCallback[, options])#

  • exportNames <string[]> 将从模块导出的名称数组。
  • evaluateCallback <Function> 在模块被评估时调用。
  • 选项
    • identifier <string> 用于堆栈跟踪的字符串。默认值: 'vm:module(i)',其中 i 是特定于上下文的递增索引。
    • context <Object>vm.createContext() 方法返回的情境化对象,用于编译和评估此 Module

创建一个新的 SyntheticModule 实例。

分配给此实例导出的对象可能会让模块的导入者访问指定 context 之外的信息。使用 vm.runInContext() 在特定上下文中创建对象。

syntheticModule.setExport(name, value)#

  • name <string> 要设置的导出名称。
  • value <any> 要设置导出的值。

此方法使用给定值设置模块导出绑定槽。

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#

ModuleRequest 表示带有给定导入属性和阶段的模块导入请求。

vm.compileFunction(code[, params[, options]])#

  • code <string> 要编译的函数体。
  • params <string[]> 一个包含函数所有参数的字符串数组。
  • options <Object>
    • filename <string> 指定此脚本产生的堆栈跟踪中使用的文件名。默认值: ''
    • lineOffset <number> 指定此脚本产生的堆栈跟踪中显示的行号偏移量。默认值: 0
    • columnOffset <number> 指定此脚本产生的堆栈跟踪中显示的首行列号偏移量。默认值: 0
    • cachedData <Buffer> | <TypedArray> | <DataView> 提供一个可选的 BufferTypedArrayDataView,其中包含所提供源码的 V8 代码缓存数据。这必须由先前使用相同的 codeparams 调用 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#

返回一个包含 VM 操作常用常量的对象。

vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER#

稳定性:1.1 - 活跃开发

一个常量,可用作 vm.Scriptvm.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>
      • strings <boolean> 如果设置为 false,任何对 eval 或函数构造函数(Function, GeneratorFunction 等)的调用都将抛出 EvalError默认值: true
      • wasm <boolean> 如果设置为 false,任何编译 WebAssembly 模块的尝试都将抛出 WebAssembly.CompileError默认值: true
    • microtaskMode <string> 如果设置为 afterEvaluate,微任务(通过 Promiseasync function 调度的任务)将在脚本通过 script.runInContext() 运行后立即运行。在这种情况下,它们被包含在 timeoutbreakOnSigint 的作用域内。
    • importModuleDynamically <Function> | <vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER> 用于指定在此上下文中调用 import() 时,当没有引用脚本或模块时,应如何加载模块。此选项是实验性模块 API 的一部分。我们不建议在生产环境中使用它。详细信息请参阅编译 API 中对动态 import() 的支持
  • 返回:<Object> 情境化对象。

如果给定的 contextObject 是一个对象,vm.createContext() 方法将准备该对象并返回对其的引用,以便它可以在对 vm.runInContext()script.runInContext() 的调用中使用。在此类脚本内部,全局对象将被 contextObject 包装,保留其所有现有属性,同时也具有任何标准全局对象所具有的内置对象和函数。在由 vm 模块运行的脚本之外,全局变量将保持不变。

const vm = require('node:vm');

global.globalVar = 3;

const context = { globalVar: 1 };
vm.createContext(context);

vm.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> 标签。

上下文提供的 nameorigin 通过 Inspector API 可见。

vm.isContext(object)#

如果给定的 object 对象已使用 vm.createContext() 进行情境化,或者如果它是使用 vm.constants.DONT_CONTEXTIFY 创建的上下文的全局对象,则返回 true

vm.measureMemory([options])#

稳定性:1 - 实验性

测量 V8 已知并由当前 V8 隔离区已知的所有上下文或主上下文使用的内存。

  • options <Object> 可选。
    • mode <string> 'summary''detailed'。在摘要模式下,只返回为主上下文测量的内存。在详细模式下,将返回为当前 V8 隔离区已知的所有上下文测量的内存。默认值: 'summary'
    • execution <string> 'default''eager'。使用默认执行时,promise 不会解析,直到下一次计划的垃圾回收开始之后,这可能需要一段时间(或者如果程序在下一次 GC 之前退出,则永远不会)。使用急切执行时,GC 将立即开始以测量内存。默认值: 'default'
  • 返回:<Promise> 如果内存成功测量,promise 将解析为一个包含内存使用信息的对象。否则,它将被拒绝,并带有 ERR_CONTEXT_NOT_INITIALIZED 错误。

返回的 Promise 可能解析的对象格式特定于 V8 引擎,并且可能随 V8 版本的不同而变化。

返回的结果与 v8.getHeapSpaceStatistics() 返回的统计信息不同,因为 vm.measureMemory() 测量 V8 引擎当前实例中每个 V8 特定上下文可达的内存,而 v8.getHeapSpaceStatistics() 的结果测量当前 V8 实例中每个堆空间占用的内存。

const vm = require('node:vm');
// Measure the memory used by the main context.
vm.measureMemory({ mode: 'summary' })
  // This is the same as vm.measureMemory()
  .then((result) => {
    // The current format is:
    // {
    //   total: {
    //      jsMemoryEstimate: 2418479, jsMemoryRange: [ 2418479, 2745799 ]
    //    }
    // }
    console.log(result);
  });

const context = vm.createContext({ a: 1 });
vm.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.a);
    // {
    //   total: {
    //     jsMemoryEstimate: 2574732,
    //     jsMemoryRange: [ 2574732, 2904372 ]
    //   },
    //   current: {
    //     jsMemoryEstimate: 2438996,
    //     jsMemoryRange: [ 2438996, 2768636 ]
    //   },
    //   other: [
    //     {
    //       jsMemoryEstimate: 135736,
    //       jsMemoryRange: [ 135736, 465376 ]
    //     }
    //   ]
    // }
    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> 提供一个可选的 BufferTypedArrayDataView,其中包含所提供源码的 V8 代码缓存数据。
    • importModuleDynamically <Function> | <vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER> 用于指定在评估此脚本期间调用 import() 时应如何加载模块。此选项是实验性模块 API 的一部分。我们不建议在生产环境中使用它。详细信息请参阅编译 API 中对动态 import() 的支持

vm.runInContext() 方法编译 code,在 contextifiedObject 的上下文中运行它,然后返回结果。运行中的代码无法访问局部作用域。contextifiedObject 对象必须先前已使用 vm.createContext() 方法进行情境化

如果 options 是一个字符串,则它指定了文件名。

以下示例使用单个情境化对象编译和执行不同的脚本

const vm = require('node:vm');

const contextObject = { globalVar: 1 };
vm.createContext(contextObject);

for (let i = 0; i < 10; ++i) {
  vm.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>
      • strings <boolean> 如果设置为 false,任何对 eval 或函数构造函数(Function, GeneratorFunction 等)的调用都将抛出 EvalError默认值: true
      • wasm <boolean> 如果设置为 false,任何编译 WebAssembly 模块的尝试都将抛出 WebAssembly.CompileError默认值: true
    • cachedData <Buffer> | <TypedArray> | <DataView> 提供一个可选的 BufferTypedArrayDataView,其中包含所提供源码的 V8 代码缓存数据。
    • importModuleDynamically <Function> | <vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER> 用于指定在评估此脚本期间调用 import() 时应如何加载模块。此选项是实验性模块 API 的一部分。我们不建议在生产环境中使用它。详细信息请参阅编译 API 中对动态 import() 的支持
    • microtaskMode <string> 如果设置为 afterEvaluate,微任务(通过 Promiseasync function 调度的任务)将在脚本运行后立即运行。在这种情况下,它们被包含在 timeoutbreakOnSigint 的作用域内。
  • 返回:<any> 脚本中执行的最后一个语句的结果。

此方法是 (new vm.Script(code, options)).runInContext(vm.createContext(options), options) 的快捷方式。如果 options 是一个字符串,则它指定了文件名。

它一次完成多项工作:

  1. 创建一个新的上下文。
  2. 如果 contextObject 是一个对象,则用新上下文将其情境化。如果 contextObject 是 undefined,则创建一个新对象并将其情境化。如果 contextObjectvm.constants.DONT_CONTEXTIFY,则不情境化任何东西。
  3. 将代码编译为 vm.Script
  4. 在创建的上下文中运行已编译的代码。代码无法访问调用此方法的作用域。
  5. 返回结果。

以下示例编译并执行增加一个全局变量并设置一个新变量的代码。这些全局变量包含在 contextObject 中。

const vm = require('node:vm');

const contextObject = {
  animal: 'cat',
  count: 2,
};

vm.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 = vm.runInNewContext('Object.freeze(globalThis); globalThis;', vm.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> 提供一个可选的 BufferTypedArrayDataView,其中包含所提供源码的 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() 函数来运行相同的代码:

const vm = require('node:vm');
let localVar = 'initial value';

const vmResult = vm.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 模块的引用。例如:

'use strict';
const vm = require('node:vm');

const code = `
((require) => {
  const http = require('node:http');

  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/');
})`;

vm.runInThisContext(code)(require); 

上述案例中的 require() 与其传递来源的上下文共享状态。当执行不受信任的代码时,这可能会引入风险,例如,以不希望的方式更改上下文中的对象。

“情境化”一个对象是什么意思?#

所有在 Node.js 中执行的 JavaScript 都在一个“上下文”的作用域内运行。根据V8 嵌入指南

在 V8 中,上下文是一个执行环境,它允许独立的、不相关的 JavaScript 应用程序在单个 V8 实例中运行。你必须显式指定你希望任何 JavaScript 代码在其中运行的上下文。

当使用一个对象调用 vm.createContext() 方法时,contextObject 参数将被用来包装一个新的 V8 上下文实例的全局对象(如果 contextObjectundefined,将在情境化之前从当前上下文创建一个新对象)。这个 V8 上下文为使用 node:vm 模块方法运行的 code 提供了一个隔离的全局环境,它可以在其中操作。创建 V8 上下文并将其与外部上下文中的 contextObject 关联的过程,就是本文档所指的“情境化”该对象。

情境化会给上下文中的 globalThis 值带来一些怪癖。例如,它不能被冻结,并且它与外部上下文中的 contextObject 不引用相等。

const vm = require('node:vm');

// An undefined `contextObject` option makes the global object contextified.
const context = vm.createContext();
console.log(vm.runInContext('globalThis', context) === context);  // false
// A contextified global object cannot be frozen.
try {
  vm.runInContext('Object.freeze(globalThis);', context);
} catch (e) {
  console.log(e); // TypeError: Cannot freeze
}
console.log(vm.runInContext('globalThis.foo = 1; foo;', context));  // 1 

要创建一个具有普通全局对象的上下文,并在外部上下文中访问一个具有较少怪癖的全局代理,请将 vm.constants.DONT_CONTEXTIFY 指定为 contextObject 参数。

vm.constants.DONT_CONTEXTIFY#

这个常量,当在 vm API 中用作 contextObject 参数时,指示 Node.js 创建一个上下文,而不以 Node.js 特定的方式将其全局对象用另一个对象包装。因此,新上下文中的 globalThis 值的行为将更接近于一个普通的值。

const vm = require('node:vm');

// Use vm.constants.DONT_CONTEXTIFY to freeze the global object.
const context = vm.createContext(vm.constants.DONT_CONTEXTIFY);
vm.runInContext('Object.freeze(globalThis);', context);
try {
  vm.runInContext('bar = 1; bar;', context);
} catch (e) {
  console.log(e); // Uncaught ReferenceError: bar is not defined
} 

vm.constants.DONT_CONTEXTIFY 用作 vm.createContext()contextObject 参数时,返回的对象是一个类似代理的对象,指向新创建的上下文中的全局对象,具有较少的 Node.js 特定怪癖。它与新上下文中的 globalThis 值引用相等,可以从上下文外部修改,并且可以直接用于访问新上下文中的内置对象。

const vm = require('node:vm');

const context = vm.createContext(vm.constants.DONT_CONTEXTIFY);

// Returned object is reference equal to globalThis in the new context.
console.log(vm.runInContext('globalThis', context) === context);  // true

// Can be used to access globals in the new context directly.
console.log(context.Array);  // [Function: Array]
vm.runInContext('foo = 1;', context);
console.log(context.foo);  // 1
context.bar = 1;
console.log(vm.runInContext('bar;', context));  // 1

// Can be frozen and it affects the inner context.
Object.freeze(context);
try {
  vm.runInContext('baz = 1; baz;', context);
} catch (e) {
  console.log(e); // Uncaught ReferenceError: baz is not defined
} 

超时与异步任务和 Promise 的交互#

Promiseasync function 可以调度由 JavaScript 引擎异步运行的任务。默认情况下,这些任务在当前堆栈上的所有 JavaScript 函数执行完毕后运行。这允许绕过 timeoutbreakOnSigint 选项的功能。

例如,以下由 vm.runInNewContext() 以 5 毫秒超时执行的代码,调度一个无限循环在 promise 解析后运行。调度的循环永远不会被超时中断:

const vm = require('node:vm');

function loop() {
  console.log('entering loop');
  while (1) console.log(Date.now());
}

vm.runInNewContext(
  'Promise.resolve().then(() => loop());',
  { loop, console },
  { timeout: 5 },
);
// This is printed *before* 'entering loop' (!)
console.log('done executing'); 

这可以通过向创建 Context 的代码传递 microtaskMode: 'afterEvaluate' 来解决:

const vm = require('node:vm');

function loop() {
  while (1) console.log(Date.now());
}

vm.runInNewContext(
  'Promise.resolve().then(() => loop());',
  { loop, console },
  { timeout: 5, microtaskMode: 'afterEvaluate' },
); 

在这种情况下,通过 promise.then() 调度的微任务将在从 vm.runInNewContext() 返回之前运行,并将被 timeout 功能中断。这仅适用于在 vm.Context 中运行的代码,因此例如 vm.runInThisContext() 不接受此选项。

Promise 回调被放入它们创建时所在上下文的微任务队列中。例如,如果上面示例中的 () => loop() 被替换为 loop,那么 loop 将被推入全局微任务队列,因为它是一个来自外部(主)上下文的函数,因此也能够逃脱超时。

如果像 process.nextTick(), queueMicrotask(), setTimeout(), setImmediate() 等异步调度函数在 vm.Context 内部可用,传递给它们的函数将被添加到所有上下文共享的全局队列中。因此,传递给这些函数的回调也无法通过超时来控制。

microtaskMode'afterEvaluate' 时,注意在上下文之间共享 Promise#

'afterEvaluate' 模式下,Context 拥有自己的微任务队列,与外部(主)上下文使用的全局微任务队列分开。虽然此模式对于强制执行 timeout 和启用带有异步任务的 breakOnSigint 是必要的,但它也使得在上下文之间共享 promise 变得具有挑战性。

在下面的示例中,一个 promise 在内部上下文中创建并与外部上下文共享。当外部上下文 await 该 promise 时,外部上下文的执行流以一种令人惊讶的方式被中断了:日志语句永远不会被执行。

import * as vm from 'node:vm';

const inner_context = vm.createContext({}, { microtaskMode: 'afterEvaluate' });

// runInContext() returns a Promise created in the inner context.
const inner_promise = vm.runInContext(
  'Promise.resolve()',
  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 规范中关于排队作业的字面规定,因为它允许来自不同上下文的异步任务以不同于它们排队顺序的顺序运行。

编译 API 中对动态 import() 的支持#

以下 API 支持 importModuleDynamically 选项,以在由 vm 模块编译的代码中启用动态 import()

  • new vm.Script
  • vm.compileFunction()
  • new vm.SourceTextModule
  • vm.runInThisContext()
  • vm.runInContext()
  • vm.runInNewContext()
  • vm.createContext()

此选项仍是实验性模块 API 的一部分。我们不建议在生产环境中使用它。

importModuleDynamically 选项未指定或为 undefined 时#

如果未指定此选项,或者它为 undefined,包含 import() 的代码仍然可以由 vm API 编译,但是当编译的代码执行并实际调用 import() 时,结果将以 ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING 拒绝。

importModuleDynamicallyvm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER#

此选项当前不支持 vm.SourceTextModule

使用此选项时,当在编译的代码中发起 import() 时,Node.js 将使用主上下文的默认 ESM 加载器来加载请求的模块,并将其返回给正在执行的代码。

这使得被编译的代码可以访问 Node.js 内置模块,如 fshttp。如果代码在不同的上下文中执行,请注意,从主上下文加载的模块创建的对象仍然来自主上下文,而不是新上下文中内置类的 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);

使用主上下文的默认加载器加载用户模块存在一些注意事项:

  1. 被解析的模块将相对于传递给 vm.Scriptvm.compileFunction()filename 选项。解析可以处理绝对路径或 URL 字符串的 filename。如果 filename 是一个既不是绝对路径也不是 URL 的字符串,或者如果它是 undefined,解析将相对于进程的当前工作目录。在 vm.createContext() 的情况下,解析始终相对于当前工作目录,因为此选项仅在没有引用脚本或模块时使用。
  2. 对于任何给定的 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.Scriptvm.runInThisContextvm.runInContextvm.runInNewContext,引用者是已编译的 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' } }
})();