Node.js v8.x 中文文档


vm (虚拟机)#

稳定性: 2 - 稳定的

vm 模块提供了一系列 API 用于在 V8 虚拟机环境中编译和运行代码。

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

A common use case is to run the code in a sandboxed environment. The sandboxed code uses a different V8 Context, meaning that it has a different global object than the rest of the code.

One can provide the context by "contextifying" a sandbox object. The sandboxed code treats any property on the sandbox like a global variable. Any changes on global variables caused by the sandboxed code are reflected in the sandbox object.

const vm = require('vm');

const x = 1;

const sandbox = { x: 2 };
vm.createContext(sandbox); // Contextify the sandbox.

const code = 'x += 40; var y = 17;';
// x and y are global variables in the sandboxed environment.
// Initially, x has the value 2 because that is the value of sandbox.x.
vm.runInContext(code, sandbox);

console.log(sandbox.x); // 42
console.log(sandbox.y); // 17

console.log(x); // 1; y is not defined.

注意: vm模块并不是实现代码安全性的一套机制。 绝不要试图用其运行未经信任的代码.

Class: vm.Script#

vm.Script类型的实例包含若干预编译的脚本,这些脚本能够在特定的沙箱(或者上下文)中被运行。

new vm.Script(code, options)#

  • code <string> 需要被解析的JavaScript代码
  • options
    • filename <string> 定义供脚本生成的堆栈跟踪信息所使用的文件名
    • lineOffset <number> 定义脚本生成的堆栈跟踪信息所显示的行号偏移
    • columnOffset <number> 定义脚本生成的堆栈跟踪信息所显示的列号偏移
    • displayErrors <boolean> 当值为真的时候,假如在解析代码的时候发生错误Error,引起错误的行将会被加入堆栈跟踪信息
    • timeout <number> 定义在被终止执行之前此code被允许执行的最大毫秒数。假如执行被终止,将会抛出一个错误[Error][]。
    • cachedData <Buffer> 为源码提供一个可选的存有v8代码缓存数据的Buffer。一旦提供了此Buffer,取决于v8引擎对Buffer中数据的接受状况,cachedDataRejected值将会被设为要么 真要么为假。
    • produceCachedData <boolean> 当值为真且cachedData不存在的时候,v8将会试图为code生成代码缓存数据。一旦成功,一个有V8代码缓存数据的Buffer将会被生成和储存在vm.Script返回的实例的cachedData属性里。 取决于代码缓存数据是否被成功生成,cachedDataProduced的值会被设置为true或者false。

创建一个新的vm.Script对象只编译代码但不会执行它。编译过的vm.Script此后可以被多次执行。code是不绑定于任何全局对象的,相反,它仅仅绑定于每次执行它的对象。

script.runInContext(contextifiedSandbox[, options])#

  • contextifiedSandbox <Object>vm.createContext()返回的[contextified][]对象
  • options <Object>
    • filename <string> 定义供脚本生成的堆栈跟踪信息所使用的文件名
    • lineOffset <number> 定义脚本生成的堆栈跟踪信息所显示的行号偏移
    • columnOffset <number> 定义脚本生成的堆栈跟踪信息所显示的列号偏移
    • displayErrors <boolean> 当值为真的时候,假如在解析代码的时候发生错误Error,引起错误的行将会被加入堆栈跟踪信息
    • timeout <number> 定义在被终止执行之前此code被允许执行的最大毫秒数。假如执行被终止,将会抛出一个错误Error
    • breakOnSigint: 若值为真,当收到SIGINT (Ctrl+C)事件时,代码会被终止执行。此外,通过process.on("SIGINT")方法所设置的消息响应机制在代码被执行时会被屏蔽,但代码被终止后会被恢复。如果执行被终止,一个错误Error会被抛出。

在指定的contextifiedSandbox中执行vm.Script对象中被编译后的代码并返回其结果。被执行的代码无法获取本地作用域。

以下的例子会编译一段代码,该代码会递增一个全局变量,给另外一个全局变量赋值。同时该代码被编译后会被多次执行。全局变量会被置于sandbox对象内。

const util = require('util');
const vm = require('vm');

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

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

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

console.log(util.inspect(sandbox));

// { animal: 'cat', count: 12, name: 'kitty' }

注意: 使用timeout或者breakOnSigint选项会导致若干新的事件循环以及对应的线程,这有一个非零的性能消耗。

script.runInNewContext([sandbox[, options]])#

  • sandbox <Object> An object that will be contextified. If undefined, a new object will be created. 一个将被[contextified][]的对象。如果是undefined, 会生成一个新的对象
  • options <Object>
    • filename <string> 定义供脚本生成的堆栈跟踪信息所使用的文件名
    • lineOffset <number> 定义脚本生成的堆栈跟踪信息所显示的行号偏移
    • columnOffset <number> 定义脚本生成的堆栈跟踪信息所显示的列号偏移
    • displayErrors <boolean> 当值为真的时候,假如在解析代码的时候发生错误Error,引起错误的行将会被加入堆栈跟踪信息
    • timeout <number> 定义在被终止执行之前此code被允许执行的最大毫秒数。假如执行被终止,将会抛出一个错误Error

首先给指定的sandbox提供一个隔离的上下文, 再在此上下文中执行vm.Script中被编译的代码,最后返回结果。运行中的代码无法获取本地作用域。

以下的例子会编译一段代码,该代码会递增一个全局变量,给另外一个全局变量赋值。同时该代码被编译后会被多次执行。全局变量会被置于各个独立的sandbox对象内。

const util = require('util');
const vm = require('vm');

const script = new vm.Script('globalVar = "set"');

const sandboxes = [{}, {}, {}];
sandboxes.forEach((sandbox) => {
  script.runInNewContext(sandbox);
});

console.log(util.inspect(sandboxes));

// [{ globalVar: 'set' }, { globalVar: 'set' }, { globalVar: 'set' }]

script.runInThisContext([options])#

  • options <Object>
    • filename <string> 定义供脚本生成的堆栈跟踪信息所使用的文件名
    • lineOffset <number> 定义脚本生成的堆栈跟踪信息所显示的行号偏移
    • columnOffset <number> 定义脚本生成的堆栈跟踪信息所显示的列号偏移
    • displayErrors <boolean> 当值为真的时候,假如在解析代码的时候发生错误Error,引起错误的行将会被加入堆栈跟踪信息
    • timeout <number> 定义在被终止执行之前此code被允许执行的最大毫秒数。假如执行被终止,将会抛出一个错误Error

在指定的global对象的上下文中执行vm.Script对象里被编译的代码并返回其结果。被执行的代码虽然无法获取本地作用域,但是能获取global对象。

以下的例子会编译一段代码,该代码会递增一个global变量。同时该代码被编译后会被多次执行。

const vm = require('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

vm.createContext([sandbox])#

给定一个sandbox对象, vm.createContext()设置此sandbox,从而让它具备在vm.runInContext()或者script.runInContext()中被使用的能力。对于此二方法中所调用的脚本,他们的全局对象不仅拥有我们提供的sandbox对象的所有属性,同时还有任何global object所拥有的属性。对于这些脚本之外的所有代码,他们的全局变量将保持不变。

const util = require('util');
const vm = require('vm');

global.globalVar = 3;

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

vm.runInContext('globalVar *= 2;', sandbox);

console.log(util.inspect(sandbox)); // { globalVar: 2 }

console.log(util.inspect(globalVar)); // 3

如果未提供sandbox(或者传入undefined),那么会返回一个全新的,空的,上下文隔离化后的sandbox对象。

vm.createContext()主要是用于创建一个能运行多个脚本的sandbox。比如说,在模拟一个网页浏览器时,此方法可以被用于创建一个单独的sandbox来代表一个窗口的全局对象,然后所有的<script>标签都可以在这个sandbox的上下文中运行。

vm.isContext(sandbox)#

当给定的sandbox对象已经被vm.createContext()上下文隔离化,则返回真。

vm.runInContext(code, contextifiedSandbox[, options])#

  • code <string> 将被编译和运行的JavaScript代码
  • contextifiedSandbox <Object> 一个被上下文隔离化过的对象,会在代码被编译和执行之后充当global对象
  • options
    • filename <string> 定义供脚本生成的堆栈跟踪信息所使用的文件名
    • lineOffset <number> 定义脚本生成的堆栈跟踪信息所显示的行号偏移
    • columnOffset <number> 定义脚本生成的堆栈跟踪信息所显示的列号偏移
    • displayErrors <boolean> 当值为真的时候,假如在解析代码的时候发生错误Error,引起错误的行将会被加入堆栈跟踪信息
    • timeout <number> 定义在被终止执行之前此code被允许执行的最大毫秒数。假如执行被终止,将会抛出一个错误Error

vm.runInContext()在指定的contextifiedSandbox的上下文里执行vm.Script对象中被编译后的代码并返回其结果。被执行的代码无法获取本地作用域。contextifiedSandbox必须是事先被vm.createContext()上下文隔离化过的对象。

以下例子使用一个单独的, 上下文隔离化过的对象来编译并运行几个不同的脚本:

const util = require('util');
const vm = require('vm');

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

for (let i = 0; i < 10; ++i) {
  vm.runInContext('globalVar *= 2;', sandbox);
}
console.log(util.inspect(sandbox));

// { globalVar: 1024 }

vm.runInDebugContext(code)#

稳定性: 0 - 失效。一个替代方案正在开发中。
  • code <string> 要被编译和执行的JavaScript代码

vm.runInDebugContext()会在V8的调试上下文中编译并执行code。此方法主要在需要获取V8Debug对象的时候使用。

const vm = require('vm')
const Debug = vm.runInDebugContext('Debug');
console.log(Debug.findScript(process.emit).name);  // 'events.js'
console.log(Debug.findScript(process.exit).name);  // 'internal/process.js'

注意: 调试上下文和对象从本质而言是从属于V8调试器的,故有可能会在没有事先警告的情况下被改变(甚至被移除)

Debug对象另外还可以通过特定于V8的--expose_debug_as命令行选项获得。

vm.runInNewContext(code[, sandbox][, options])#

  • code <string> 将被编译和运行的JavaScript代码
  • sandbox <Object> 一个将被上下文隔离化的对象。如果是undefined, 会生成一个新的对象

  • options

    • filename <string> 定义供脚本生成的堆栈跟踪信息所使用的文件名
    • lineOffset <number> 定义脚本生成的堆栈跟踪信息所显示的行号偏移
    • columnOffset <number> 定义脚本生成的堆栈跟踪信息所显示的列号偏移
    • displayErrors <boolean> 当值为真的时候,假如在解析代码的时候发生错误Error,引起错误的行将会被加入堆栈跟踪信息
    • timeout <number> 定义在被终止执行之前此code被允许执行的最大毫秒数。假如执行被终止,将会抛出一个错误Error

首先给指定的sandbox(若为undefined,则会新建一个sandbox)提供一个隔离的上下文, 再在此上下文中执行vm.Script中被编译的代码,最后返回结果。运行中的代码无法获取本地作用域。

以下的例子会编译一段代码,该代码会递增一个全局变量,给另外一个全局变量赋值。同时该代码被编译后会被多次执行。全局变量会被置于sandbox对象内。

const util = require('util');
const vm = require('vm');

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

vm.runInNewContext('count += 1; name = "kitty"', sandbox);
console.log(util.inspect(sandbox));

// { animal: 'cat', count: 3, name: 'kitty' }

vm.runInThisContext(code[, options])#

  • code <string> 将被编译和运行的JavaScript代码
  • options
    • filename <string> 定义供脚本生成的堆栈跟踪信息所使用的文件名
    • lineOffset <number> 定义脚本生成的堆栈跟踪信息所显示的行号偏移
    • columnOffset <number> 定义脚本生成的堆栈跟踪信息所显示的列号偏移
    • displayErrors <boolean> 当值为真的时候,假如在解析代码的时候发生错误Error,引起错误的行将会被加入堆栈跟踪信息
    • timeout <number> 定义在被终止执行之前此code被允许执行的最大毫秒数。假如执行被终止,将会抛出一个错误Error

vm.runInThisContext()在当前的global对象的上下文中编译并执行code,最后返回结果。运行中的代码无法获取本地作用域,但可以获取当前的global对象。

下面的例子演示了使用vm.runInThisContext()和JavaScript的eval()方法去执行相同的一段代码:

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

const vmResult = vm.runInThisContext('localVar = "vm";');
console.log('vmResult:', vmResult);
console.log('localVar:', localVar);

const evalResult = eval('localVar = "eval";');
console.log('evalResult:', evalResult);
console.log('localVar:', localVar);

// vmResult: 'vm', localVar: 'initial value'
// evalResult: 'eval', localVar: 'eval'

正因vm.runInThisContext()无法获取本地作用域,故localVar的值不变。相反,eval()确实能获取本地作用域,所以localVar的值被改变了。如此看来,vm.runInThisContext()更像是间接的执行eval(), 就像(0, eval)('code')

Example: Running an HTTP Server within a VM#

在使用script.runInThisContext()或者vm.runInThisContext()时,目标代码是在当前的V8全局对象的上下文中执行的。被传入此虚拟机上下文的目标代码会有自己独立的作用域。

要想用http模块搭建一个简易的服务器,被传入的代码必须要么自己执行require('http'),要么引用一个http,比如:

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

const code = `
((require) => {
  const http = require('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()和导出它的上下文共享状态。这在运行未经认证的代码时可能会引入风险,比如在不理想的情况下修改上下文中的对象。

What does it mean to "contextify" an object?#

所有用Node.js所运行的JavaScript代码都是在一个“上下文”的作用域中被执行的。 根据V8 Embedder's Guide

在V8中,一个上下文是一个执行环境,它允许分离的,无关的JavaScript应用在一个V8的单例中被运行。 你必须明确地指定用于运行所有JavaScript代码的上下文。

当调用vm.createContext()时,传入的sandbox对象(或者新建的一个sandbox对象,若原sandboxundefined)在底层会和一个新的V8上下文实例联系上。这个V8上下文在一个隔离的全局环境中,使用vm模块的方法运行code。创建V8上下文和使之联系上sandbox的过程在此文档中被称作为"上下文隔离化"sandbox