当 importModuleDynamically 为 vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER 时


【When importModuleDynamically is vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER

该选项目前不支持 vm.SourceTextModule

【This option is currently not supported for vm.SourceTextModule.】

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

【With this option, when an import() is initiated in the compiled code, Node.js would use the default ESM loader from the main context to load the requested module and return it to the code being executed.】

这使得被编译的代码可以访问 Node.js 内置模块,例如 fshttp。如果代码在不同的上下文中执行,需要注意的是,从主上下文加载的模块创建的对象仍然来自主上下文,并且在新上下文中并不是内置类的 instanceof 实例。

【This gives access to Node.js built-in modules such as fs or http to the code being compiled. If the code is executed in a different context, be aware that the objects created by modules loaded from the main context are still from the main context and not instanceof built-in classes in the new context.】

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);

此选项还允许脚本或函数加载用户模块:

【This option also allows the script or function to load user modules:】

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);

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

【There are a few caveats with loading user modules using the default loader from the main context:】

  1. 要解析的模块将相对于传递给 vm.Scriptvm.compileFunction()filename 选项。解析可以使用绝对路径或 URL 字符串作为 filename。如果 filename 是既不是绝对路径也不是 URL 的字符串,或者未定义,则解析将相对于进程的当前工作目录。在 vm.createContext() 的情况下,解析始终相对于当前工作目录,因为该选项仅在没有引用脚本或模块时使用。
  2. 对于任何解析为特定路径的 filename,一旦进程成功从该路径加载了特定模块,结果可能会被缓存,并且随后从同一路径加载相同模块时会返回相同的内容。如果 filename 是 URL 字符串,且包含不同的查询参数,则缓存不会被命中。对于非 URL 字符串的 filename,目前没有办法绕过缓存行为。