You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 03:35:20