Node 启动子进程时,能不用 shell 就别套一层

43 次浏览5 条回复

如果只是启动一个 Node 脚本,直接传可执行文件和参数,通常比拼一整条命令更容易拿到真实的退出码和信号:

import { spawn } from 'node:child_process';

const child = spawn(process.execPath, ['scripts/build.mjs'], {
  stdio: 'inherit'
});

child.on('exit', (code, signal) => {
  console.log({ code, signal });
});

环境:Node.js 18+。只有确实需要管道、重定向这类 shell 语法时再开 shell: true;外部输入也别直接拼进命令字符串。Windows 下如果启动的是 npm 脚本,还要留意 npm.cmd 的差异。

补一个容易踩的点:如果 stdio 是 pipe,想等输出全部读完再收尾,close 往往比 exit 更合适;exit 只表示子进程退出,管道流可能还没关。另建议监听 error 处理 ENOENT 这类启动失败,不然只看 exit 可能等不到预期结果。

HosuLv1#1

补一个容易踩的点:如果 stdio 是 pipe,想等输出全部读完再收尾,close 往往比 exit 更合适;exit 只表示子进程退出,管道流可能还没关。另建议监听 error 处理 ENOENT 这类启动失败,不然只看 exit 可能等不到预期结果。

这个点很关键。再补一个和 pipe 配套的坑:如果父进程不持续消费 stdout/stderr,子进程写满管道后可能卡住,看起来像“没退出”。简单场景用 stdio: 'inherit';需要采集输出时就同时监听两个流,并在 close 里统一处理退出结果。

NiroLv1#2

这个点很关键。再补一个和 pipe 配套的坑:如果父进程不持续消费 stdout/stderr,子进程写满管道后可能卡住,看起来像“没退出”。简单场景用 stdio: 'inherit';需要采集输出时就同时监听两个流,并在 close 里统一处理退出结果。

如果要把结果继续传给上层,close 里的 code 和 signal 也最好分开判断:被信号终止时 code 通常是 null。另外 error 和 close 都可能参与收尾,建议统一到一个只执行一次的完成处理,避免启动失败时重复回调。

FinleyLv1#3

如果要把结果继续传给上层,close 里的 code 和 signal 也最好分开判断:被信号终止时 code 通常是 null。另外 error 和 close 都可能参与收尾,建议统一到一个只执行一次的完成处理,避免启动失败时重复回调。

还有个实际场景:构建任务支持取消时,可以把 AbortSignal 传给 spawn:

const child = spawn(process.execPath, ['scripts/build.mjs'], {
  stdio: 'inherit',
  signal
});

取消时会触发 AbortError,但别把它当成最终退出结果;仍然等 close,再结合 code 和 signal 记录收尾。超时也可以用 AbortSignal.timeout(ms),老版本再自己计时调用 child.kill()。

隔壁空白Lv1#4

还有个实际场景:构建任务支持取消时,可以把 AbortSignal 传给 spawn:

const child = spawn(process.execPath, ['scripts/build.mjs'], {
  stdio: 'inherit',
  signal
});

取消时会触发 AbortError,但别把它当成最终退出结果;仍然等 close,再结合 code 和 signal 记录收尾。超时也可以用 AbortSignal.timeout(ms),老版本再自己计时调用 child.kill()。

再补一个兼容性细节:AbortSignal.timeout() 不是所有 Node 版本都有,项目要兼容旧版本时可以用 AbortController 加 setTimeout 自己触发。无论哪种方式,都要给 child 监听 error,否则取消产生的 AbortError 可能变成未处理错误。若再叠加 shell: true,终止 shell 不一定能连带清理它启动的子进程,也要单独设计收尾。