07. child_process 工作原理概览
本章解决的问题
node:child_process 让当前 Node.js 进程启动另一个操作系统进程。它常用于执行 Bash、Python、R、FFmpeg,也能启动另一个 Node.js 程序。
父 Node.js 进程(独立 PID、内存、事件循环)
├─ 写入 child.stdin ──> 子进程 stdin
├─ 读取 child.stdout <── 子进程 stdout
├─ 读取 child.stderr <── 子进程 stderr
└─ 可选 IPC <──> 消息通道
子进程不会自动共享父进程的变量、数据库连接、EventEmitter、定时器或 Express 请求对象。创建时,它通常会继承父进程环境变量和工作目录,但之后两边各自独立。
四组 API
import {
exec,
execFile,
fork,
spawn,
} from "node:child_process";
| API | 默认经过 Shell | 返回输出 | 适合场景 |
|---|---|---|---|
spawn() |
否 | 流 | 大输出、长任务、实时进度 |
execFile() |
否 | 一次性回调或 Promise | 明确程序、少量输出 |
exec() |
是 | 一次性回调或 Promise | 固定且可信的短 Shell 命令 |
fork() |
否 | 流 + IPC | 启动 Node.js 子模块 |
存在同步版本 spawnSync()、execFileSync()、execSync()。它们会阻塞当前事件循环,不应放在 Express 请求处理中。
最小示例
import { spawn } from "node:child_process";
const child = spawn(process.execPath, ["--version"], {
stdio: ["ignore", "pipe", "pipe"],
});
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk: string) => {
process.stdout.write(`Node 版本:${chunk}`);
});
child.on("error", (error: Error) => {
console.error("无法启动子进程:", error.message);
});
child.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
console.log({ code, signal });
});
使用 process.execPath 可以确保子进程使用与父进程相同的 Node.js 可执行文件,避免 PATH 中存在多个 Node.js 版本。
生命周期时间线
正常完成:
spawn() 返回 ChildProcess
→ "spawn"
→ stdout/stderr 若干 "data"
→ "exit"
→ stdio 全部关闭
→ "close"
命令不存在:
spawn() 返回 ChildProcess
→ "error"(例如 ENOENT)
→ "close"
spawn() 返回对象不代表程序已成功启动;必须监听 error。
常见错误
// 错误:以为变量会在子进程中共享。
let progress = 0;
spawn(process.execPath, ["worker.js"]);
// worker.js 无法直接读取这里的 progress。
// 错误:在 HTTP 请求中同步执行长任务,整个进程会停止处理请求。
import { execFileSync } from "node:child_process";
execFileSync("python", ["slow.py"]);
最佳实践
- 默认优先
spawn()或execFile(),不要为了方便随意启用 Shell。 - 命令和参数分开传递,不拼接用户输入。
- 同时监听启动错误和最终关闭。
- 明确 stdout、stderr、退出码组成的脚本协议。
- 大输出使用流,不在内存中无限累积。
- 为长任务设计超时、取消和清理。
练习
- 用
spawn()执行process.execPath --version,输出子进程 PID。 - 创建子脚本,分别向 stdout、stderr 写内容,并以退出码 2 结束。
- 解释为什么父子进程不能共享一个内存
Map。
验收清单
- [ ] 能说明父进程和子进程的内存、PID、事件循环相互独立。
- [ ] 能按场景初步选择
spawn()、execFile()、exec()、fork()。 - [ ] 知道同步 API 会阻塞事件循环。
- [ ] 知道
spawn()返回不等于启动成功。