为什么我们需要 Child Process?
Node.js 的设计哲学是“事件驱动、非阻塞 I/O”,这使得它在处理高并发网络请求时表现优异。然而,Node.js 本质上是单线程的。
这意味着什么?意味着它非常不擅长 CPU 密集型任务(如视频转码、大规模数据计算、复杂的图像处理)。如果主线程被这些任务阻塞,整个应用就会“假死”,无法响应任何其他请求。
child_process(子进程)模块就是为了解决这个问题而生的。它允许你生成新的进程,利用操作系统的多核 CPU 能力,或者直接执行系统 Shell 命令,而不会阻塞 Node.js 的主线程。
第一部分:四大核心方法 (The Big Four)
Node.js 提供了四种创建子进程的方式,它们各司其职。
1. spawn() —— 处理流 (Stream) 的首选
这是最基础、最强大的方法。它不会一次性加载所有数据,而是以流 (Stream) 的形式返回数据。
- 适用场景: 处理大量数据(如读取几个 G 的日志)、长时间运行的进程。
- 特点: 内存占用小,没有缓冲区上限风险。
- 是否创建 Shell: 否(默认直接调用命令,更安全)。
const { spawn } = require('child_process');
// 例子:执行 'ls -lh /usr'
const ls = spawn('ls', ['-lh', '/usr']);
// 监听标准输出(数据是一块一块流出来的)
ls.stdout.on('data', (data) => {
console.log(`输出: ${data}`);
});
// 监听错误输出
ls.stderr.on('data', (data) => {
console.error(`错误: ${data}`);
});
// 监听进程结束(推荐监听 close 而不是 exit)
ls.on('close', (code) => {
console.log(`子进程退出,退出码 ${code}`);
});
2. exec() —— 快速执行 Shell 命令
它会创建一个 Shell(如 /bin/sh 或 cmd.exe),执行命令,并将产生的输出缓冲 (Buffer) 起来,最后一次性回调给函数。
- 适用场景: 执行返回结果较短的简单命令(如
git status)。 - 缺点: 如果输出数据超过默认缓冲区大小(默认 1MB),进程会直接崩溃。
- 是否创建 Shell: 是(支持管道
|等 Shell 语法)。
const { exec } = require('child_process');
exec('ls -lh | grep .json', (error, stdout, stderr) => {
if (error) {
console.error(`执行出错: ${error}`);
return;
}
console.log(`结果: ${stdout}`);
});
3. execFile() —— 执行可执行文件
类似于 exec(),但它默认不创建 Shell,而是直接执行指定的文件。
- 适用场景: 执行
.sh脚本、.exe文件或具体的二进制程序。 - 特点: 比
exec更高效(省去了 Shell 开销),且更安全(避免 Shell 注入)。
const { execFile } = require('child_process');
execFile('node', ['--version'], (error, stdout, stderr) => {
if (error) throw error;
console.log(stdout);
});
4. fork() —— Node 进程专用
这是 spawn() 的一个特例,专门用于派生新的 Node.js 进程。
- 核心卖点: 它会在父子进程间建立一个 IPC (进程间通信) 通道,允许互发 JSON 消息。
- 代价: 每个进程都是独立的 V8 实例,内存消耗较大(30MB+ 起步)。
// parent.js
const { fork } = require('child_process');
const child = fork('./child.js');
child.send({ msg: '启动计算任务' }); // 发送消息
child.on('message', (m) => console.log('父进程收到:', m)); // 接收消息
决策速查表
| 方法 | 核心机制 | 是否开启 Shell | 适合场景 | 关键限制 |
|---|---|---|---|---|
| spawn | Stream (流) | 否 (默认) | 大文件、长耗时任务 | 需手动处理流事件 |
| exec | Buffer (缓冲) | 是 | 简单命令 | maxBuffer (1MB) 限制 |
| execFile | Buffer (缓冲) | 否 | 执行脚本/程序 | 同样受 maxBuffer 限制 |
| fork | IPC 通道 | 否 | Node.js 多进程计算 | 资源开销大 |
第二部分:参数详解 (The Options Object)
很多人只关注 command,却忽略了 options 对象。这才是控制子进程行为的关键。
1. 通用高频参数
-
cwd(Current Working Directory)- 作用: 指定子进程在哪个目录下运行。
- 场景: 比如在特定项目文件夹下执行
npm install。
-
env(Environment Variables)- 作用: 指定子进程的环境变量。
- 🚫 致命坑点: 这个参数是覆盖而非合并。如果你只传了自定义变量,子进程将丢失系统
PATH,导致连ls都找不到。 - ✅ 正确写法:
{ env: { ...process.env, MY_VAR: '123' } }
-
stdio(Standard I/O) —— 最重要配置- 作用: 决定子进程的日志去哪里。
'pipe'(默认):建立管道,父进程通过代码读取。'inherit':直接复用父进程终端。子进程的console.log直接打印在你的屏幕上,适合编写 CLI 工具。'ignore':丢弃输出。
2. exec / execFile 特有参数
-
maxBuffer- 默认值:
1024 * 1024(1MB) - 作用: stdout 或 stderr 允许的最大字节数。
- 警告: 如果你的命令输出超过这个值(比如读取了一个大文件),子进程会抛出
RangeError并崩溃。如果预估输出很大,请改用spawn或调大此值。
- 默认值:
-
timeout- 默认值:
0(无限制) - 作用: 毫秒数。如果子进程运行超时,Node 会自动将其 Kill 掉。
- 默认值:
3. spawn 高级参数
-
detached- 作用: 让子进程脱离父进程独立运行。
- 场景: 编写常驻后台的守护进程。通常需要配合
child.unref()使用,否则父进程无法退出。
4. shell 与安全
-
shell- 类型: Boolean 或 String (Shell 路径)
- 作用: 是否在 Shell 中运行。
exec默认为true。 - 安全警告: 开启 Shell 后,如果命令中拼接了用户输入,极易导致 Shell 注入攻击。例如用户输入
; rm -rf /。 - 最佳实践: 尽可能使用
spawn或execFile并通过数组传递参数,而不是拼接字符串。
第三部分:实战避坑指南
-
监听结束事件选
close还是exit?exit: 进程退出了,但管道里可能还有数据没流完。close: stdio 流已经完全关闭。- 结论: 绝大多数情况下,监听
close更稳妥。
-
Windows 上的黑框框
- 在 Windows 上调用
spawn或exec有时会弹出一个瞬间消失的cmd黑框。 - 解决: 设置
options: { windowsHide: true }。
- 在 Windows 上调用
-
僵尸进程
- 在使用
spawn时,如果不正确处理流(例如不消费 stdout),某些操作系统可能会因为管道缓冲区填满而挂起子进程。确保你始终在处理流,或者设置stdio: 'ignore'。
- 在使用
总结
Node.js 的 child_process 是连接底层系统能力的桥梁。
- 处理大流量、长任务
spawn - 简单命令、小结果
exec - 执行特定程序
execFile - Node.js 多核计算
fork
掌握了这些,你就不再受限于 Node.js 的单线程模型,可以构建出更健壮、更强大的服务端应用。
参考资料:本文核心定义与 API 规范参考自 Node.js 中文文档 - Child Process