本章关注的是不可信代码的执行。在这种情况下,“不可信”指的是不属于您的应用或导入模块的一部分,但仍然可以执行的代码。本章特别关注运行不可信代码的两个主要用例。第一种涉及通过产生子进程来执行应用和脚本。这个用例允许 Node 应用表现得像一个 shell 脚本,编排多个实用程序来实现一个更大的目标。第二个用例涉及 JavaScript 源代码的执行。虽然这种场景不像流程派生那样常见,但它在 Node 核心中受到支持,应该理解为eval()的替代方案。
child_process模块
用于产生子进程并与其交互的child_process核心模块 提供了几种运行这些进程的方法,每种方法提供不同级别的控制和实现复杂性。本节解释了每种方法的工作原理,并指出了每种方法的优缺点。
exec()
exec()方法 可能是启动子进程最简单的方法。exec()方法将命令(例如,从命令行发出的命令)作为其第一个参数。当exec()被调用时,一个新的 shell——在 Windows 中为cmd.exe,否则为/bin/sh——被启动并用于执行命令字符串。额外的配置选项可以通过可选的第二个参数传递给exec()。该参数(如果存在)应该是包含表 9-1 中所示的一个或多个属性的对象。
表 9-1 。exec()支持的配置选项
|
财产
|
描述
|
| --- | --- |
| cwd | 用于设置子进程工作目录的值。 |
| env | env应该是一个对象,它的键值对指定子进程的环境。这个对象相当于子对象中的process.env。如果未指定,子进程将从父进程继承其环境。 |
| encoding | 子进程的stdout和stderr流使用的字符编码。默认为utf8 (UTF-8) |
| timeout | 用于在一定时间后终止子进程的属性。如果该值大于 0,进程将在timeout毫秒后终止。否则,该过程将无限期运行。该属性默认为 0。 |
| maxBuffer | 子进程的stdout或stderr流中可以缓冲的最大数据量。默认为 200 KB。如果任何一个流超过了这个值,子进程就会被终止。 |
| killSignal | 用于终止子进程的信号。例如,如果发生超时或者超过了最大缓冲区大小,它将被发送到子进程。默认为SIGTERM。 |
exec()的最后一个参数是子进程终止后调用的回调函数。这个函数通过三个参数调用。按照 Node 约定,第一个参数是任何错误条件。论成功,这个论点就是null。如果存在错误,参数就是Error的一个实例。第二个和第三个参数是来自子进程的缓冲的stdout和stderr数据。因为回调是在子进程终止后调用的,所以stdout和stderr参数不是流,而是包含子进程执行时通过流传递的数据的字符串。stdout和stderr各可保存总共maxBuffer字节。清单 9-1 显示了一个使用exec()的例子,它执行ls命令(Windows 用户可以替换为dir)来显示根目录的内容(注意这个例子没有使用配置选项参数)。清单 9-2 显示了一个等价的例子,一个传递配置选项的例子。在第二个示例中,要列出的目录不再在实际的命令字符串中指定。但是,cwd选项用于将工作目录设置为根目录。尽管清单 9-1 和 9-2 的输出应该是相同的,但是它们将取决于您本地机器的内容。
清单 9-1 。使用exec()显示过程的输出
var cp = require("child_process");
cp.exec("ls -l /", function(error, stdout, stderr) {
if (error) {
console.error(error.toString());
} else if (stderr !== "") {
console.error(stderr);
} else {
console.log(stdout);
}
});清单 9-2 。相当于清单 9-1 中的显示(带有配置选项)
var cp = require("child_process");
cp.exec("ls -l", {
cwd: "/"
}, function(error, stdout, stderr) {
if (error) {
console.error(error.toString());
} else if (stderr !== "") {
console.error(stderr);
} else {
console.log(stdout);
}
});execFile()
execFile()方法 与exec()类似,有两点细微区别。第一个是execFile()不会产生新的壳。相反,execFile()直接执行传递给它的文件,使得execFile()比exec()消耗的资源稍微少一些。第二个区别是execFile()的第一个参数是要执行的文件的名称,没有其他参数。清单 9-3 显示了如何调用ls命令来显示当前工作目录的内容。
清单 9-3 。使用execFile()执行没有附加参数的文件
var cp = require("child_process");
cp.execFile("ls", function(error, stdout, stderr) {
if (error) {
console.error(error.toString());
} else if (stderr !== "") {
console.error(stderr);
} else {
console.log(stdout);
}
});
警告因为execFile()没有产生新的 shell,Windows 用户无法让它发出dir之类的命令。在 Windows 中,dir是 shell 的内置功能。另外,execFile()不能用于运行.cmd和.bat文件,它们依赖于 shell。然而,您可以使用execFile()来运行.exe文件。
如果需要向命令传递额外的参数,可以指定一个参数数组作为execFile()的第二个参数。清单 9-4 展示了这是如何完成的。在本例中,再次执行ls命令。然而,这次还传入了-l标志和/来显示根目录的内容。
清单 9-4 。向由execFile()执行的文件传递参数
var cp = require("child_process");
cp.execFile("ls", ["-l", "/"], function(error, stdout, stderr) {
if (error) {
console.error(error.toString());
} else if (stderr !== "") {
console.error(stderr);
} else {
console.log(stdout);
}
});第三个参数——或者第二个,如果没有命令参数传入的话——是可选的配置对象。由于execFile()支持与exec()相同的选项,可以从表 9-1 中获得对所支持属性的解释。清单 9-5 中的例子使用了配置对象的cwd选项,在语义上等同于清单 9-4 中的代码。
清单 9-5 。相当于清单 9-4 中的,它利用了cwd选项
var cp = require("child_process");
cp.execFile("ls", ["-l"], {
cwd: "/"
}, function(error, stdout, stderr) {
if (error) {
console.error(error.toString());
} else if (stderr !== "") {
console.error(stderr);
} else {
console.log(stdout);
}
});
注意在幕后,exec()调用execFile(),用你操作系统的 shell 作为文件参数。然后,要执行的命令被传递给数组参数中的execFile()。
spawn()T3】
exec()和execFile()方法很简单,当您只需要发出一个命令并捕获它的输出时,它们工作得很好。然而,一些应用需要更复杂的交互。这就是spawn()发挥作用的地方,它是 Node 为子进程提供的最强大、最灵活的抽象(从开发人员的角度来看,它也需要做最多的工作)。spawn()也被execFile()——引申为exec()——以及fork()(本章后面会讲到)。
spawn()最多接受三个参数。第一个是要执行的命令,它应该只是可执行文件的路径。它不应该包含命令的任何参数。若要向命令传递参数,请使用可选的第二个参数。如果存在,它应该是要传递给命令的值的数组。第三个也是最后一个参数,也是可选的,用于将选项传递给spawn()本身。表 9-2 列出了spawn()支持的选项。
表 9-2 。spawn()支持的选项列表
|
财产
|
描述
|
| --- | --- |
| cwd | 用于设置子进程工作目录的值。 |
| env | env应该是一个对象,它的键值对指定子进程的环境。这个对象相当于子对象中的process.env。如果未指定,子进程将从父进程继承其环境。 |
| stdio | 用于配置子进程的标准流的数组或字符串。这一论点将在下文阐述。 |
| detached | 一个布尔值,指定子进程是否将成为进程组领导。如果true,即使父终止,子也可以继续执行。这默认为false。 |
| uid | 这个数字代表运行进程的用户身份,允许程序作为另一个用户运行并临时提升特权。默认为null,使子进程作为当前用户运行。 |
| gid | 用于设置进程组标识的数字。默认为null,根据当前用户设置。 |
stdio选项
stdio选项 用于配置子进程的stdin、stdout和stderr流。该选项可以是一个三项数组或以下字符串之一:"ignore"、"pipe"和"inherit"。在解释字符串参数之前,必须先理解数组形式。如果stdio是一个数组,第一个元素为子进程的stdin流设置文件描述符。类似地,第二个和第三个元素分别为孩子的stdout和stderr流设置文件描述符。表 9-3 列举了每个数组元素的可能值。
表 9-3 。stdio 数组条目的可能值
|
价值
|
描述
|
| --- | --- |
| "pipe" | 在子进程和父进程之间创建管道。spawn()返回一个ChildProcess对象(稍后将详细解释)。父对象可以通过ChildProcess对象的stdin、stdout和stderr流访问子对象的标准流。 |
| "ipc" | 在子进程和父进程之间创建一个进程间通信(IPC)通道,用于传递消息和文件描述符。一个子进程最多可以有一个 IPC 文件描述符。(IPC 通道将在后面的章节中详细介绍。) |
| "ignore" | 导致子级的相应流被忽略。 |
| 流对象 | 可以与子进程共享的可读或可写的流。流的基础文件描述符在子进程中是重复的。例如,父进程可以建立一个子进程来从文件流中读取命令。 |
| 正整数 | 对应于与子进程共享的父进程中当前打开的文件描述符。 |
| null或undefined | 分别对stdin、stdout和stderr使用默认值 0、1 和 2。 |
如果stdio是字符串,可以是"ignore"、"pipe"或"inherit"。这些值是某些阵列配置的简写。各值的含义如表 9-4 所示。
表 9-4 。每个 stdio 字符串值的翻译
|
线
|
价值
|
| --- | --- |
| "ignore" | ["ignore", "ignore", "ignore"] |
| "pipe" | ["pipe", "pipe", "pipe"] |
| "inherit" | [process.stdin, process.stdout, process.stderr]或[0, 1, 2] |
ChildProcess类
spawn()不接受exec()``execFile()等回调函数。相反,它返回一个ChildProcess对象。ChildProcess类继承自EventEmitter,用于与衍生的子进程交互。ChildProcess对象提供了三个流对象stdin、stdout和stderr,代表底层子流程的标准流。清单 9-6 中的例子使用spawn()来运行根目录中的ls命令。然后子进程被设置为从父进程继承它的标准流。因为子级的标准流被连接到父级的流,所以子级的输出被打印到控制台。因为我们唯一真正感兴趣的是ls命令的输出,所以stdio选项也可以使用数组["ignore", process.stdout, "ignore"]来设置。
清单 9-6 。使用spawn()执行命令
var cp = require("child_process");
var child = cp.spawn("ls", ["-l"], {
cwd: "/",
stdio: "inherit"
});
注意为了复习使用标准流,请重温第 5 章和第 7 章。这一章着重于前面没有提到的内容。
在最后一个例子中,子进程的stdout流基本上是通过使用stdio属性的"inherit"值来管理的。然而,该流也可以被显式控制。清单 9-7 中的例子直接接入子进程的stdout流及其data事件处理程序。
var cp = require("child_process");
var child = cp.spawn("ls", ["-l", "/"]);
child.stdout.on("data", function(data) {
process.stdout.write(data.toString());
});error事件
当不能产生或杀死子对象时,或者当向它发送 IPC 消息失败时,ChildProcess对象发出一个error事件。ChildProcess error事件处理程序的通用格式如清单 9-8 所示。
清单 9-8 。一个事件处理程序
var cp = require("child_process");
var child = cp.spawn("ls");
child.on("error", function(error) {
// process error here
console.error(error.toString());
});exit事件
当子进程终止时,ChildProcess对象发出一个exit事件。向exit事件处理程序传递了两个参数。第一个是进程被父进程终止时的退出代码(如果进程没有被父进程终止,代码参数为null))。第二个是用来杀死进程的信号。如果子进程没有被来自父进程的信号终止,这也是null。清单 9-9 显示了一个通用的exit事件处理程序。
清单 9-9 。一个事件处理程序
var cp = require("child_process");
var child = cp.spawn("ls");
child.on("exit", function(code, signal) {
console.log("exit code: " + code);
console.log("exit signal: " + signal);
});close事件
当子进程的标准流关闭时,发出close事件 。这不同于exit事件,因为多个进程可能共享相同的流。像exit事件一样,close也提供退出代码和信号作为事件处理程序的参数。清单 9-10 中显示了一个通用的close事件处理程序。
清单 9-10 。一个事件处理程序
var cp = require("child_process");
var child = cp.spawn("ls");
child.on("close", function(code, signal) {
console.log("exit code: " + code);
console.log("exit signal: " + signal);
});pid属性
一个ChildProcess的pid属性 用于获取子进程的标识符。清单 9-11 显示了如何访问pid属性。
清单 9-11 。访问子进程的pid属性
var cp = require("child_process");
var child = cp.spawn("ls");
console.log(child.pid);kill()T2】
kill() 用于向子进程发送信号。这个给孩子的信号是kill()的唯一论据。如果没有提供参数,kill()发送SIGTERM信号试图终止子进程。在清单 9-12 中调用kill()的例子中,还包含了一个exit事件处理程序来显示终止信号。
清单 9-12 。使用kill()向子进程发送信号
var cp = require("child_process");
var child = cp.spawn("cat");
child.on("exit", function(code, signal) {
console.log("Killed using " + signal);
});
child.kill("SIGTERM");fork()T2】
fork()``spawn()的特例,用于创建 Node 流程(见清单 9-13 )。modulePath参数是运行在子进程中的 Node 模块的路径。可选的第二个参数是一个数组,用于将参数传递给子进程。最后一个参数是一个可选对象,用于将选项传递给fork()。fork()支持的选项如表 9-5 所示。
清单 9-13 。使用child_process.fork()方法
child_process.fork(modulePath, [args], [options])表 9-5 。fork()支持的选项
|
[计]选项
|
描述
|
| --- | --- |
| cwd | 用于设置子进程工作目录的值。 |
| env | env应该是一个对象,它的键值对指定子进程的环境。该对象相当于子对象中的process.env。如果未指定,子进程将从父进程继承其环境。 |
| encoding | 子进程使用的字符编码。默认为"utf8" (UTF-8)。 |
注fork()返回的流程是 Node 的新实例,包含 V8 的完整实例。注意不要创建太多这样的进程,因为它们会消耗大量资源。
由fork()返回的ChildProcess对象配备了内置的 IPC 通道,允许不同的 Node 进程通过 JSON 消息进行通信。默认情况下,子流程的标准流也与父流程相关联。
为了演示fork()如何工作,需要两个测试应用。第一个应用(见清单 9-14 )代表要执行的子模块。该模块只是打印传递给它的参数、它的环境和它的工作目录。将这段代码保存在名为child.js的文件中。
清单 9-14 。子模块
console.log("argv: " + process.argv);
console.log("env: " + JSON.stringify(process.env, null, 2));
console.log("cwd: " + process.cwd());清单 9-15 显示了相应的父进程。这段代码派生出 Node 的一个新实例,它运行清单 9-14 中的child模块。对fork()的调用传递了一个-foo参数给孩子。它还将孩子的工作目录设置为/,并提供自定义环境。当应用运行时,子进程的打印语句显示在父进程的控制台上。
清单 9-15 。清单 9-14 中显示的子模块的父模块
var cp = require("child_process");
var child;
child = cp.fork(__dirname + "/child", ["-foo"], {
cwd: "/",
env: {
bar: "baz"
}
});send()
send()方法 使用内置的 IPC 通道在 Node 进程间传递 JSON 消息。父进程可以通过调用ChildProcess对象的send()方法来发送数据。然后,通过在process对象上设置一个message事件处理程序,可以在子流程中处理数据。类似地,子 Node 可以通过调用process.send()方法向其父 Node 发送数据。在父流程中,数据通过ChildProcess的message事件处理程序接收。
以下示例包含两个 Node 应用,它们无限期地来回传递消息。子模块(见清单 9-16 )应该存储在一个名为message-counter.js的文件中。整个模块就是process对象的message处理程序。每次收到消息时,处理程序都会显示消息计数器。接下来,我们通过检查process.connected的值来验证父进程仍然存在,并且 IPC 通道完好无损。如果信道被连接,计数器递增,并且消息被发送回父进程。
清单 9-16 。将消息传递回其父模块的子模块
process.on("message", function(message) {
console.log("child received: " + message.count);
if (process.connected) {
message.count++;
process.send(message);
}
});清单 9-17 显示了相应的父进程。父进程首先派生一个子进程,然后设置两个事件处理程序。第一个处理来自子进程的message事件。处理器显示消息计数,并检查 IPC 通道是否通过child.connected值连接。如果是,处理程序递增计数器,然后将消息传递回子进程。
第二个处理器监听SIGINT信号。如果收到了SIGINT,子进程被杀死,父进程退出。添加这个处理程序是为了允许用户终止两个程序,这两个程序正在一个无限的消息传递循环中运行。在清单 9-17 的末尾,通过向孩子发送一个计数为 0 的消息来开始消息传递。要测试这个程序,只需运行父进程。要终止,只需按下Ctrl+C。
清单 9-17 。与清单 9-16 中的子模块协同工作的父模块
var cp = require("child_process");
var child = cp.fork(__dirname + "/message-counter");
child.on("message", function(message) {
console.log("parent received: " + message.count);
if (child.connected) {
message.count++;
child.send(message);
}
});
child.on("SIGINT", function() {
child.kill();
process.exit();
});
child.send({
count: 0
});
注意如果通过send()传输的对象有一个名为cmd的属性,其值是一个以"NODE_"开头的字符串,那么消息不会作为message事件发出。对象{cmd: "NODE_foo"}就是一个例子。这些是 Node 核心使用的特殊消息,并导致发出internalMessage事件。官方文档强烈反对使用此功能,因为它可能会在不通知的情况下更改。
disconnect()
要关闭父进程和子进程之间的 IPC 通道,使用disconnect()方法。从父流程中,调用ChildProcess的disconnect()方法。从子进程来看,disconnect()是process对象的一个方法。
disconnect(),不接受任何参数,导致几件事情发生。首先,在父进程和子进程中将ChildProcess.connected和process.connected设置为false。第二,在两个进程中都发出了一个disconnect事件。一旦disconnect()被调用,试图发送更多消息将导致错误。
清单 9-18 显示了一个只包含一个disconnect事件处理程序的子模块。当父进程断开连接时,子进程会向控制台打印一条消息。将这段代码存储在一个名为disconnect.js的文件中。清单 9-19 显示了相应的父进程。父进程派生一个子进程,设置一个disconnect事件处理程序,然后立即与子进程断开连接。当disconnect事件由子进程发出时,父进程也会向控制台输出一条再见消息。
清单 9-18 。实现disconnect事件处理程序的子模块
process.on("disconnect", function() {
console.log("Goodbye from the child process");
});清单 9-19 。与清单 9-18 中的所示的子 Node 相对应的父 Node
var cp = require("child_process");
var child = cp.fork(__dirname + "/disconnect");
child.on("disconnect", function() {
console.log("Goodbye from the parent process");
});
child.disconnect();vm模块
vm(虚拟机)核心模块 用于执行 JavaScript 代码的原始字符串。乍一看,它似乎只是 JavaScript 内置eval()函数的另一种实现,但是vm要强大得多。对于初学者来说,vm允许你解析一段代码并在以后运行它——这是用eval()做不到的。vm还允许您定义代码执行的上下文,使其成为eval()的更安全的替代方案。关于vm,上下文是由一个全局对象和一组内置对象和函数组成的 V8 数据结构。代码执行的上下文可以被认为是 JavaScript 环境。本节的剩余部分描述了vm为使用上下文和执行代码提供的各种方法。
注意 eval(),一个不与任何对象关联的全局函数,以一个字符串作为唯一的参数。这个字符串可以包含任意的 JavaScript 代码,eval()将试图执行这些代码。由eval()执行的代码拥有与调用者相同的特权,以及对当前作用域内任何变量的访问权。eval()被认为是一个安全风险,因为它让任意代码对您的数据进行读/写访问,,通常应该避免。
runInThisContext()
runInThisContext()方法允许代码使用与应用其余部分相同的上下文来执行。这个方法有两个参数。第一个是要执行的代码字符串。可选的第二个参数表示所执行代码的“文件名”。如果存在,这可以是任何字符串,因为它只是一个虚拟文件名,用于提高堆栈跟踪的可读性。清单 9-20 是一个使用runInThisContext()打印到控制台的简单例子。结果输出如清单 9-21 中的所示。
清单 9-20 。使用vm.runInThisContext()
var vm = require("vm");
var code = "console.log(foo);";
foo = "Hello vm";
vm.runInThisContext(code);清单 9-21 。清单 9-20 中的代码生成的输出
$ node runInThisContext-hello.js
Hello vm由runInThisContext()执行的代码可以访问与您的应用相同的上下文,这意味着它可以访问所有全局定义的数据。但是,执行代码不能访问非全局变量。这大概是runInThisContext()和eval()最大的区别。为了说明这个概念,首先看看清单 9-22 中的例子,它从runInThisContext()内部访问全局变量foo。回想一下,没有使用var关键字声明的 JavaScript 变量会自动成为全局变量。
清单 9-22 。在vm.runInThisContext()内更新全局变量
var vm = require("vm");
var code = "console.log(foo); foo = 'Goodbye';";
foo = "Hello vm";
vm.runInThisContext(code);
console.log(foo);清单 9-23 显示了运行清单 9-22 中代码的输出。在这个例子中,变量foo最初保存值"Hello vm"。当runInThisContext()被执行时,foo被打印到控制台,然后赋值"Goodbye"。最后,再次打印出foo的值。在runInThisContext()内发生的分配持续存在,并且Goodbye被打印。
清单 9-23 。清单 9-22 中的代码产生的输出
$ node runInThisContext-update.js
Hello vm
Goodbye如前所述,runInThisContext()不能访问非全局变量。清单 9-22 在清单 9-24 中被重写,因此foo现在是一个局部变量(使用var关键字声明)。另外,请注意,指定可选文件名的附加参数现在已经被传递到了runInThisContext()中。
清单 9-24 。试图访问vm.runInThisContext()中的非全局变量
var vm = require("vm");
var code = "console.log(foo);";
var foo = "Hello vm";
vm.runInThisContext(code, "example.vm");当执行清单 9-24 中的代码时,试图访问foo时会出现ReferenceError。异常和堆栈跟踪如列表 9-25 所示。注意堆栈跟踪引用了example.vm,与runInThisContext()相关的文件名。
清单 9-25 。清单 9-24 中代码的堆栈跟踪输出
$ node runInThisContext-var.js
/home/colin/runInThisContext-var.js:5
vm.runInThisContext(code, "example.vm");
^
ReferenceError: foo is not defined
at example.vm:1:13
at Object.<anonymous> (/home/colin/runInThisContext-var.js:5:4)
at Module._compile (module.js:456:26)
at Object.Module._extensions..js (module.js:474:10)
at Module.load (module.js:356:32)
at Function.Module._load (module.js:312:12)
at Function.Module.runMain (module.js:497:10)
at startup (node.js:119:16)
at node.js:901:3清单 9-26 用对eval()的调用替换了对runInThisContext()的调用。结果输出也显示在清单 9-27 中。根据观察到的输出,eval()显然能够在本地范围内访问foo。
清单 9-26 。使用eval()成功访问局部变量
var vm = require("vm");
var code = "console.log(foo);";
var foo = "Hello eval";
eval(code);清单 9-27 。清单 9-26 的输出结果
$ node runInThisContext-eval.js
Hello evalrunInNewContext()T2】
在上一节中,您看到了如何通过使用runInThisContext()而不是eval()来保护局部变量。然而,因为runInThisContext()与当前的上下文一起工作,它仍然允许不可信的代码访问您的全局数据。如果你需要进一步限制访问,使用vm的runInNewContext()方法。顾名思义,runInNewContext()创建了一个全新的上下文,代码可以在其中执行。清单 9-28 显示了runInNewContext()的用法。第一个参数是要执行的 JavaScript 字符串。第二个可选参数用作新上下文中的全局对象。第三个参数也是可选的,是堆栈跟踪中显示的文件名。
清单 9-28 。使用vm.runInNewContext()
vm.runInNewContext(code, [sandbox], [filename])sandbox参数用于设置上下文中的全局变量,以及在runInNewContext()完成后检索值。记住,使用runInThisContext(),我们能够直接修改全局变量,并且这些改变会持续下去。然而,因为runInNewContext()使用了一组不同的全局变量,所以同样的技巧并不适用。例如,人们可能期望清单 9-29 中的代码在运行时显示"Hello vm",但事实并非如此。
清单 9-29 。试图使用vm.runInNewContext()执行代码
var vm = require("vm");
var code = "console.log(foo);";
foo = "Hello vm";
vm.runInNewContext(code);这段代码没有成功运行,而是崩溃了,错误如清单 9-30 中的所示。出现错误是因为新的上下文无权访问应用的console对象。值得指出的是,程序崩溃前只抛出一个错误。然而,即使console可用,也会抛出第二个异常,因为全局变量foo在新的上下文中不可用。
清单 9-30 。清单 9-29 中的代码抛出的ReferenceError
ReferenceError: console is not defined幸运的是,我们可以使用sandbox参数显式地将foo和console对象传递给新的上下文。清单 9-31 展示了如何完成这个任务。运行时,这段代码如预期的那样显示"Hello vm"。
清单 9-31 。vm.runInNewContext()的成功运用
var vm = require("vm");
var code = "console.log(foo);";
var sandbox;
foo = "Hello vm";
sandbox = {
console: console,
foo: foo
};
vm.runInNewContext(code, sandbox);沙盒数据
关于runInNewContext()的一件好事是,对沙盒数据所做的更改实际上不会改变应用的数据。在清单 9-32 所示的例子中,全局变量foo和console通过沙箱传递给runInNewContext()。在runInNewContext()内部,定义了一个名为bar的新变量,foo被打印到控制台,然后foo被修改。在runInNewContext()完成之后,foo会被再次打印,同时还有几个沙箱值。
清单 9-32 。创建和修改沙盒数据
var vm = require("vm");
var code = "var bar = 1; console.log(foo); foo = 'Goodbye'";
var sandbox;
foo = "Hello vm";
sandbox = {
console: console,
foo: foo
};
vm.runInNewContext(code, sandbox);
console.log(foo);
console.log(sandbox.foo);
console.log(sandbox.bar);清单 9-33 显示了结果输出。"Hello vm"的第一个实例来自于runInNewContext()内部的 print 语句。不出所料,这是通过沙箱传入的foo的值。接下来,foo被设置为"Goodbye"。但是,下一个打印语句显示的是foo的原始值。这是因为runInNewContext()内部的赋值语句更新了foo的沙盒副本。最后两条 print 语句反映了foo ( "Goodbye")和bar (1)在runInNewContext()末尾的沙箱值。
清单 9-33 。清单 9-32 的输出结果
$ node runInNewContext-sandbox.js
Hello vm
Hello vm
Goodbye
1runInContext()T2】
Node 允许您创建单独的 V8 上下文对象,并使用runInContext()方法在其中执行代码。使用vm的createContext()方法创建单独的上下文。runInContext()可以不带参数调用,导致它返回一个空的上下文。或者,沙盒对象可以传递给createContext(),它被浅层复制到上下文的全局对象。createContext()的用法如清单 9-34 所示。
清单 9-34 。使用vm.createContext()
vm.createContext([initSandbox])由createContext()返回的上下文对象可以作为第二个参数传递给vm的runInContext()方法,这与runInNewContext()几乎相同。唯一的区别是runInContext()的第二个参数是一个上下文对象,而不是沙箱。清单 9-35 显示了如何使用runInContext()重写清单 9-32 。不同之处在于runInContext()取代了runInNewContext()和用createContext()创建的context,,取代了sandbox变量。运行这段代码的输出与清单 9-33 中显示的相同。
清单 9-35 。使用vm.createContext()重写清单 9-34
var vm = require("vm");
var code = "var bar = 1; console.log(foo); foo = 'Goodbye'";
var context;
foo = "Hello vm";
context = vm.createContext({
console: console,
foo: foo
});
vm.runInContext(code, context);
console.log(foo);
console.log(context.foo);
console.log(context.bar);createScript()
createScript()方法 ,用于编译一个 JavaScript 字符串以备将来执行,当你想多次执行代码时,这个方法很有用。createScript()方法接受两个参数,该方法返回一个无需重新解释代码就可以重复执行的vm.Script对象。首先是要编译的代码。可选的第二个参数表示将在堆栈跟踪中显示的文件名。
createScript()返回的vm.Script对象有三种执行代码的方法。这些方法是runInThisContext()、runInNewContext()、runInContext()的修改版本。这三种方法的用法如清单 9-36 所示。它们的行为与同名的vm方法相同。不同之处在于,这些方法不接受 JavaScript 代码字符串或文件名参数,因为它们已经是脚本对象的一部分。
清单 9-36 。vm.Script类型的脚本执行方法
script.runInThisContext()
script.runInNewContext([sandbox])
script.runInContext(context)清单 9-37 显示了一个在循环中多次运行脚本的例子。在这个例子中,使用createScript()编译了一个简单的脚本。接下来,使用设置为 0 的单个值i创建沙箱。然后使用runInNewContext()在一个for循环中执行该脚本十次。每次迭代都会增加i的沙箱值。当循环完成时,沙箱被打印出来。当显示沙箱时,增量操作的累积效果是明显的,因为i的值是 10。
清单 9-37 。多次执行已编译的脚本
var vm = require("vm");
var script = vm.createScript("i++;", "example.vm");
var sandbox = {
i: 0
}
for (var i = 0; i < 10; i++) {
script.runInNewContext(sandbox);
}
console.log(sandbox);
// displays {i: 10}摘要
本章向您展示了如何以各种方式执行代码。首先讨论的是程序需要执行另一个应用的常见情况。在这些情况下,使用child_process模块中的方法。详细检查了方法exec()、execFile()、spawn()和fork(),以及每种方法提供的不同抽象级别。接下来将介绍 JavaScript 代码字符串的执行。探索了vm模块,并将其各种方法与 JavaScript 的原生eval()函数进行了比较。还涵盖了上下文的概念和由vm提供的各种类型的上下文。最后,您学习了如何编译脚本并在以后使用vm.Script类型执行它们。