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

最佳实践

练习

  1. spawn() 执行 process.execPath --version,输出子进程 PID。
  2. 创建子脚本,分别向 stdout、stderr 写内容,并以退出码 2 结束。
  3. 解释为什么父子进程不能共享一个内存 Map

验收清单