VM(执行 JavaScript)#

稳定性:2 - 稳定

源代码: lib/vm.js

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

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

JavaScript 代码可以立即编译和运行,也可以编译、保存并在以后运行。

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

可以通过上下文化对象来提供上下文。 调用的代码将上下文中的任何属性都视为全局变量。 调用的代码对全局变量的任何更改都会反映在上下文对象中。

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> 提供一个可选的 BufferTypedArray,或带有 V8 的代码缓存数据的 DataView,用于提供的源。 如果提供,则 cachedDataRejected 值将设置为 truefalse,具体取决于 V8 对数据的接受情况。
    • 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#

当将 cachedData 提供给创建 vm.Script 时,此值将设置为 truefalse,具体取决于 V8 对数据的接受情况。 否则,该值为 undefined

script.createCachedData()#

创建一个代码缓存,该缓存可与 Script 构造函数的 cachedData 选项一起使用。 返回 Buffer。 可以在任何时间和任意次数调用此方法。

Script 的代码缓存不包含任何 JavaScript 可观察状态。 代码缓存可以安全地与脚本源一起保存,并用于多次构造新的 Script 实例。

Script 源中的函数可以标记为延迟编译,并且在构造 Script 时不会对其进行编译。 这些函数将在第一次调用时进行编译。 代码缓存序列化 V8 当前了解的关于 Script 的元数据,V8 可以使用该元数据来加速将来的编译。

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,要么是将要 contextified 的对象。 如果是 undefined,为了向后兼容,将创建一个空的 contextified 对象。
  • 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。 origin 应该格式化为 URL,但仅包含 scheme、host 和 port(如果需要),例如 url.origin 属性的值。 最值得注意的是,这个字符串应该省略尾部的斜杠,因为它表示一个路径。 默认值: ''
    • contextCodeGeneration <Object>
      • strings <boolean> 如果设置为 false,则任何对 eval 或函数构造函数(FunctionGeneratorFunction 等)的调用都将抛出一个 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 是一个对象,则使用新上下文 contextifies 它。 如果 contextObject 未定义,则创建一个新对象并 contextifies 它。 如果 contextObjectvm.constants.DONT_CONTEXTIFY,则不要 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 magic comment 的源编译时,此属性将设置为 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.Script 对象的同步性质相反,对 vm.Module 对象的操作本质上是异步的。 使用 'async' 函数可以帮助操作 vm.Module 对象。

使用 vm.Module 对象需要三个不同的步骤:创建/解析、链接和评估。 以下示例说明了这三个步骤。

此实现低于 ECMAScript 模块加载器 的级别。 目前也无法与 Loader 进行交互,尽管计划提供支持。

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 bar = new vm.SourceTextModule(`
  import s from 'foo';
  s;
  print(s);
`, { context: contextifiedObject });

// Step 2
//
// "Link" the imported dependencies of this Module to it.
//
// The provided linking callback (the "linker") accepts two arguments: the
// parent module (`bar` in this case) and the string that is the specifier of
// the imported module. The callback is expected to return a Module that
// corresponds to the provided specifier, with certain requirements documented
// in `module.link()`.
//
// If linking has not started for the returned Module, the same linker
// callback will be called on the returned Module.
//
// Even top-level Modules without dependencies must be explicitly linked. The
// callback provided would never be called, however.
//
// The link() method returns a Promise that will be resolved when all the
// Promises returned by the linker resolve.
//
// Note: This is a contrived example in that the linker function 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.

async function linker(specifier, referencingModule) {
  if (specifier === 'foo') {
    return 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 });

    // Using `contextifiedObject` instead of `referencingModule.context`
    // here would work as well.
  }
  throw new Error(`Unable to resolve dependency: ${specifier}`);
}
await bar.link(linker);

// Step 3
//
// Evaluate the Module. The evaluate() method returns a promise which will
// resolve after the module has finished evaluating.

// Prints 42.
await bar.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 bar = new vm.SourceTextModule(`
    import s from 'foo';
    s;
    print(s);
  `, { context: contextifiedObject });

  // Step 2
  //
  // "Link" the imported dependencies of this Module to it.
  //
  // The provided linking callback (the "linker") accepts two arguments: the
  // parent module (`bar` in this case) and the string that is the specifier of
  // the imported module. The callback is expected to return a Module that
  // corresponds to the provided specifier, with certain requirements documented
  // in `module.link()`.
  //
  // If linking has not started for the returned Module, the same linker
  // callback will be called on the returned Module.
  //
  // Even top-level Modules without dependencies must be explicitly linked. The
  // callback provided would never be called, however.
  //
  // The link() method returns a Promise that will be resolved when all the
  // Promises returned by the linker resolve.
  //
  // Note: This is a contrived example in that the linker function 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.

  async function linker(specifier, referencingModule) {
    if (specifier === 'foo') {
      return 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 });

      // Using `contextifiedObject` instead of `referencingModule.context`
      // here would work as well.
    }
    throw new Error(`Unable to resolve dependency: ${specifier}`);
  }
  await bar.link(linker);

  // Step 3
  //
  // Evaluate the Module. The evaluate() method returns a promise which will
  // resolve after the module has finished evaluating.

  // Prints 42.
  await bar.evaluate();
})();

module.dependencySpecifiers#

此模块的所有依赖项的说明符。 返回的数组被冻结,以禁止对其进行任何更改。

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

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>

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

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

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

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

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

链接器函数大致对应于 ECMAScript 规范中实现定义的 HostResolveImportedModule 抽象操作,但有一些关键差异。

在模块链接期间使用的实际 HostResolveImportedModule 实现是返回链接期间链接的模块的实现。 由于那时所有模块都应该已经被完全链接,因此 HostResolveImportedModule 实现完全符合规范。

对应于 ECMAScript 规范中 Cyclic Module RecordLink() 具体方法 字段。

module.namespace#

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

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

module.status#

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

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

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

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

  • 'evaluating':正在通过自身或父模块上的 module.evaluate() 来评估模块。

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

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

除了 'errored' 之外,此状态字符串对应于规范的 Cyclic Module Record[[Status]] 字段。 'errored' 对应于规范中的 'evaluated',但 [[EvaluationError]] 设置为非 undefined 的值。

类:vm.SourceTextModule#

稳定性: 1 - 实验性的

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

vm.SourceTextModule 类提供 Source Text Module Record,如 ECMAScript 规范中所定义。

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

  • code <字符串(string)> 要解析的 JavaScript 模块代码
  • options
    • identifier <字符串(string)> 用于堆栈跟踪的字符串。 默认: 'vm:module(i)',其中 i 是上下文特定的递增索引。
    • cachedData <Buffer> | <TypedArray> | <DataView> 提供可选的 BufferTypedArray, 或 DataView,其中包含 V8 的代码缓存数据以用于提供的源代码。 code 必须与创建此 cachedData 的模块相同。
    • context <对象(Object)>vm.createContext() 方法返回的 contextified 对象,用于在此 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 = {};
    },
  });
// Since module has no dependencies, the linker function will never be called.
await module.link(() => {});
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 = {};
      },
    });
  // Since module has no dependencies, the linker function will never be called.
  await module.link(() => {});
  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 }); 

类:vm.SyntheticModule#

稳定性: 1 - 实验性的

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

vm.SyntheticModule 类提供 Synthetic Module Record,如 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])#

创建一个新的 SyntheticModule 实例。

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

syntheticModule.setExport(name, value)#

此方法在链接模块后用于设置导出的值。 如果在链接模块之前调用它,则会抛出 ERR_VM_MODULE_STATUS 错误。

import vm from 'node:vm';

const m = new vm.SyntheticModule(['x'], () => {
  m.setExport('x', 1);
});

await m.link(() => {});
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.link(() => {});
  await m.evaluate();
  assert.strictEqual(m.namespace.x, 1);
})();

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

将给定的代码编译到提供的上下文中(如果未提供上下文,则使用当前上下文),并将其包装在具有给定 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,要么是将要 contextified 的对象。 如果是 undefined,为了向后兼容,将创建一个空的 contextified 对象。
  • options <Object>
    • name <string> 新创建的上下文的可读名称。默认: 'VM 上下文 i',其中 i 是已创建上下文的递增数字索引。
    • origin <string> 与新创建的上下文对应的 ,用于显示目的。 源的格式应类似于 URL,但仅包含 scheme、host 和 port(如果需要),如 url.origin 属性的值 URL 对象。 最值得注意的是,此字符串应省略尾部斜杠,因为它表示路径。 默认: ''
    • codeGeneration <Object>
      • strings <boolean> 如果设置为 false,则任何对 eval 或函数构造函数(FunctionGeneratorFunction 等)的调用都将抛出一个 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 isolate 或主上下文已知的所有上下文使用的内存。

  • options <Object> 可选。
    • mode <string> 'summary''detailed'。 在摘要模式下,只会返回为主上下文测量的内存。 在详细模式下,将返回为当前 V8 isolate 已知的所有上下文测量的内存。 默认: 'summary'
    • execution <string> 'default''eager'。 使用默认执行,Promise 将在下一次计划的垃圾回收开始后才会解析,这可能需要一段时间(或者如果程序在下一次 GC 之前退出,则永远不会)。 使用 eager 执行,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> 提供可选的 BufferTypedArray,或具有 V8 代码缓存数据的 DataView,用于提供的源。
    • 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,要么是将要 contextified 的对象。 如果是 undefined,为了向后兼容,将创建一个空的 contextified 对象。
  • 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。 origin 应该格式化为 URL,但仅包含 scheme、host 和 port(如果需要),例如 url.origin 属性的值。 最值得注意的是,这个字符串应该省略尾部的斜杠,因为它表示一个路径。 默认值: ''
    • contextCodeGeneration <Object>
      • strings <boolean> 如果设置为 false,则任何对 eval 或函数构造函数(FunctionGeneratorFunction 等)的调用都将抛出一个 EvalError默认值: true
      • wasm <boolean> 如果设置为 false,则任何编译 WebAssembly 模块的尝试都将抛出一个 WebAssembly.CompileError默认值: true
    • cachedData <Buffer> | <TypedArray> | <DataView> 提供可选的 BufferTypedArray,或具有 V8 代码缓存数据的 DataView,用于提供的源。
    • 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 是一个对象,则使用新上下文 contextifies 它。 如果 contextObject 未定义,则创建一个新对象并 contextifies 它。 如果 contextObjectvm.constants.DONT_CONTEXTIFY,则不要 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> 提供可选的 BufferTypedArray,或具有 V8 代码缓存数据的 DataView,用于提供的源。
    • 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'); 

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

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 中可用,则传递给它们的函数将被添加到由所有上下文共享的全局队列中。因此,传递给这些函数的回调也不能通过超时来控制。

在编译 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,则包含 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 选项。解析可以使用 filename,它是一个绝对路径或 URL 字符串。如果 filename 是一个既不是绝对路径也不是 URL 的字符串,或者如果它是未定义的,则解析将相对于进程的当前工作目录。对于 vm.createContext(),解析始终相对于当前工作目录,因为此选项仅在没有引用脚本或模块时使用。
  2. 对于解析为特定路径的任何给定 filename,一旦进程设法从该路径加载特定模块,结果可能会被缓存,并且随后从同一路径加载同一模块将返回相同的内容。如果 filename 是一个 URL 字符串,如果它有不同的搜索参数,则不会命中缓存。对于不是 URL 字符串的 filename,目前没有办法绕过缓存行为。

importModuleDynamically 是一个函数时#

importModuleDynamically 是一个函数时,当在编译后的代码中调用 import() 时,将调用该函数,以便用户可以自定义应如何编译和评估请求的模块。目前,必须使用 --experimental-vm-modules 标志启动 Node.js 实例才能使此选项生效。如果未设置该标志,则将忽略此回调。如果评估的代码实际调用 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 的编译的 Functionnew vm.SourceTextModule 的编译的 vm.SourceTextModule,以及 vm.createContext() 的上下文 Object
  • importAttributes <Object> 传递给 optionsExpression 可选参数的 "with" 值,如果没有提供值,则为空对象。
  • phase <string> 动态导入的阶段("source""evaluation")。
  • 返回:<模块命名空间异构对象> | <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' } }
})();