如何在Node.js脚本中正确启停服务器子进程(React/Express测试)
在Node.js中管理并正确关闭多个子进程的方案
核心问题分析
你遇到的残留进程、脚本无法退出问题,本质是exec会创建中间shell,实际的服务器进程是shell的子进程,直接杀死shell进程时,信号没有传递到子进程;另外,未监听子进程的退出事件,导致主脚本无法感知子进程是否彻底终止。
解决方案1:使用spawn替代exec(推荐)
spawn不创建中间shell,直接启动目标进程,更容易追踪和管理子进程,避免进程树嵌套问题。
import { spawn } from 'child_process'; import type { ChildProcessWithoutNullStreams } from 'child_process'; // 启动Express API服务器 const startApiServer = async (): Promise<ChildProcessWithoutNullStreams> => { return new Promise((resolve, reject) => { // 直接启动服务器脚本,跳过npm run的中间shell const child = spawn('node', ['./server.js'], { stdio: 'inherit' }); // 监听启动成功的标识输出 child.stdout?.on('data', (data) => { const output = data.toString(); if (output.includes('App listening on port')) { resolve(child); } }); child.on('error', reject); }); }; // 启动React开发服务器 const startReactDevServer = async (): Promise<ChildProcessWithoutNullStreams> => { return new Promise((resolve, reject) => { // 切换到React项目目录启动开发服务 const child = spawn('npm', ['run', 'start'], { cwd: './client', stdio: 'inherit' }); child.stdout?.on('data', (data) => { const output = data.toString(); // React开发服务启动成功的典型标识 if (output.includes('Compiled successfully')) { resolve(child); } }); child.on('error', reject); }); }; // 测试流程主函数 const runTests = async () => { console.log('Starting servers...'); // 并行启动两个服务器 const [apiServer, reactServer] = await Promise.all([ startApiServer(), startReactDevServer() ]); console.log('Both servers are ready. Running tests...'); // 这里执行你的测试逻辑 // await yourTestSuite(); console.log('Tests finished. Shutting down servers...'); // 先尝试优雅关闭(SIGTERM),给服务器清理资源的机会 apiServer.kill('SIGTERM'); reactServer.kill('SIGTERM'); // 等待所有子进程彻底退出,确保主脚本能正常结束 await Promise.all([ new Promise(resolve => apiServer.on('exit', resolve)), new Promise(resolve => reactServer.on('exit', resolve)) ]); console.log('All servers shut down successfully.'); }; runTests().catch(err => { console.error('Error occurred:', err); process.exit(1); });
解决方案2:处理exec的进程树(兼容原有逻辑)
如果必须使用exec,可以通过tree-kill工具杀死整个进程树(需先安装:npm install tree-kill),确保所有子进程被彻底终止。
import { exec } from 'child_process'; import treeKill from 'tree-kill'; import type { ChildProcess } from 'child_process'; const startNode = async (): Promise<ChildProcess> => { return new Promise((resolve, reject) => { const child = exec('npm run start:dev:norestart'); if (!child.stdout) return reject(new Error('Failed to access child process stdout')); child.stdout.on('data', (data) => { console.log(data.toString()); if (data.toString().includes('App listening on port')) { resolve(child); } }); child.on('error', reject); }); }; const doThing = async () => { console.log('Start'); const child = await startNode(); console.log('Performing operations...'); // 执行你的测试操作 console.log('Shutting down...'); // 杀死整个进程树,确保所有子进程被终止 treeKill(child.pid, 'SIGTERM', (err) => { if (err) { console.error('Failed to gracefully shut down, forcing termination:', err); treeKill(child.pid, 'SIGKILL'); } console.log('Finished'); process.exit(0); }); }; doThing().catch(err => { console.error(err); process.exit(1); });
关键注意事项
- 优先用
spawn:避免中间shell带来的进程树嵌套问题,简化进程管理。 - 监听退出事件:确保主脚本在子进程彻底退出后再结束,防止脚本提前退出导致残留进程。
- 优雅关闭优先:先发送
SIGTERM信号让服务器清理资源,失败再用SIGKILL强制终止。 - React开发服务器特殊处理:React的
react-scripts start会启动多个子进程,必须杀死整个进程树才能彻底关闭。
内容的提问来源于stack exchange,提问作者Alec Mather
相关产品推荐
相关产品推荐

