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