child_process.spawn() 是 Node.js/Electron 中用于创建子进程的核心方法,它提供了最灵活的子进程控制方式,特别适合处理大量数据或需要流式输入输出的场景。

一、spawn() 的核心功能

  1. 启动新进程:直接启动指定的可执行文件或命令
  2. 流式 I/O:通过流(Stream)的方式处理子进程的输入输出
  3. 精细控制:可以独立控制 stdin、stdout 和 stderr
  4. 无缓冲处理:不像 exec() 那样缓冲整个输出,适合大数据量

二、方法签名

const { spawn } = require('child_process');
const child = spawn(command[, args][, options]);

三、参数详解

1. command (必需)

  • 类型:string
  • 说明:要执行的命令或可执行文件路径
  • 最佳实践​:
  • 使用绝对路径确保可靠性
  • 在 Windows 上注意可执行文件扩展名

2. args (可选)

  • 类型:Array<string>
  • 默认值:[]
  • 特殊用法:
// 安全参数传递(避免注入)
const args = [
  '--input',
  sanitize(userInput), // 必须对用户输入进行消毒
  '--output',
  '/safe/path'
];

3. options (可选)

完整配置选项表:

选项 类型 默认值 重要说明
cwd string null 设置子进程工作目录
env Object process.env 定制环境变量
argv0 string undefined 显式设置 argv[0]
stdio Array/string 'pipe' 控制 stdio 配置
detached boolean false 使子进程独立
uid number - 设置用户身份 (Unix)
gid number - 设置组身份 (Unix)
shell boolean/string false 指定 shell 路径
windowsHide boolean true 隐藏 Windows 控制台窗口

关键选项详解​:

  1. stdio 配置:

三种标准流

  • stdin (标准输入):流编号 0
  • stdout (标准输出):流编号 1
  • stderr (标准错误):流编号 2

配置形式

  • stdio 可以配置为:字符串(简写形式); 数组(精细控制每个流)

字符串简写形式

描述 等效数组形式
'pipe' 创建管道(默认值) ['pipe', 'pipe', 'pipe']
'ignore' 忽略子进程的 stdio ['ignore', 'ignore', 'ignore']
'inherit' 继承父进程的 stdio [process.stdin, process.stdout, process.stderr]
  • 使用示例:
const { spawn } = require('child_process');

// 继承父进程的 stdio(子进程输出会显示在父进程控制台)
spawn('node', ['child.js'], { stdio: 'inherit' });

// 完全忽略子进程的 stdio
spawn('node', ['child.js'], { stdio: 'ignore' });

// 创建管道(默认行为)
spawn('node', ['child.js'], { stdio: 'pipe' });

数组精细配置

数组形式可以分别配置 stdin、stdout 和 stderr,甚至可以配置额外的文件描述符。

  • 每个位置可接受的值类型
值类型 描述 示例
'pipe' 创建管道 'pipe'
'ignore' 忽略该流 'ignore'
'inherit' 继承父进程对应流 'inherit'
Stream 对象 使用现有的流 process.stderr
文件描述符 使用现有文件描述符 fs.openSync('log.txt', 'w')
null/undefined 使用默认值('pipe') null
  • 基础配置示例
// 分别配置三个标准流
spawn('node', ['child.js'], {
  stdio: [
    'pipe',     // stdin (0)
    'ignore',   // stdout (1)
    process.stderr // stderr (2) 继承父进程的 stderr
  ]
});
  • 高级文件重定向示例
const fs = require('fs');
const { spawn } = require('child_process');

// 将 stdout 和 stderr 重定向到文件
const out = fs.openSync('./out.log', 'a');
const err = fs.openSync('./err.log', 'a');

const child = spawn('node', ['child.js'], {
  stdio: [
    'pipe', // stdin
    out,    // stdout 写入文件
    err     // stderr 写入文件
  ]
});
  • 额外文件描述符(fd 3+)

数组长度可以超过 3,配置额外的文件描述符:

const fs = require('fs');
const { spawn } = require('child_process');

// 创建额外的 IPC 通道
const child = spawn('node', ['child.js'], {
stdio: ['pipe', 'pipe', 'pipe', 'pipe'] // fd 3 用于自定义通信
});

// 子进程可以通过 process.stdio[3] 访问这个通道
child.stdio[3].write('Hello from parent');
  1. shell 选项:
  • false:直接执行命令(推荐安全用法)
  • true:使用系统默认 shell
  • 字符串:指定 shell 路径
  • 安全警告​:启用 shell 可能导致命令注入漏洞

四、使用注意事项

1. 安全最佳实践

  • 永远不要直接拼接用户输入​:
// 危险!可能被注入
spawn(`convert ${userInput} output.jpg`, { shell: true });

// 安全做法
spawn('convert', [
  sanitize(userInput), // 必须消毒
'output.jpg'
], { shell: false });
  • 文件路径处理​:
const { resolve } = require('path');
// 不安全
spawn('ls', [userProvidedPath]);

// 安全
const safePath = resolve(BASE_DIR, normalize(userProvidedPath));
spawn('ls', [safePath]);

2. 资源管理

  • 进程泄漏防护​:
const processes = new Set();
function safeSpawn(...args) {
  const child = spawn(...args);
  processes.add(child);

  child.on('exit', () => {
    processes.delete(child);
  });

  return child;
}

// 应用退出时清理
app.on('before-quit', () => {
  processes.forEach(child => child.kill());
});
  • 流处理必做事项​:
const child = spawn('long-running-process');

// 必须处理 error 事件防止内存泄漏
child.stdout.on('data', () => { /*...*/ });
child.stdout.on('error', (err) => {
  console.error('Stdout error:', err);
});

// 不使用的流必须销毁
child.stdin.end();

3. 跨平台兼容性

  • 路径分隔符处理​:
const path = require('path');
const cmd = process.platform === 'win32'
   ? 'cmd.exe'
   : 'bash';

const args = process.platform === 'win32'
  ? ['/c', 'dir']
  : ['-c', 'ls'];
  • 可执行文件扩展名​:
function getPlatformExecutable(name) {
  if (process.platform === 'win32') {
    return `${name}.exe`;
  }
  return name;
}

spawn(getPlatformExecutable('myapp'));

五、完整用法示例

基础示例:列出目录内容

const { spawn } = require('child_process');

// Unix-like、OHOS系统
const ls = spawn('ls', ['-lh', '/usr']);

// Windows 系统
// const dir = spawn('cmd.exe', ['/c', 'dir', 'C:\\']);

ls.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

ls.stderr.on('data', (data) => {
  console.error(`stderr: ${data}`);
});

ls.on('close', (code) => {
  console.log(`子进程退出,退出码 ${code}`);
});

六、高级配置技巧

1. 使用 shell 语法

// 启用 shell 语法(如管道、重定向等)
const child = spawn('find . -type f | wc -l', {
  shell: true,
  stdio: 'inherit'  // 继承父进程的 stdio
});

2. 分离长时间运行的进程

const child = spawn('node', ['server.js'], {
  detached: true,
  stdio: 'ignore'
});

// 允许父进程退出而子进程继续运行
child.unref();

3. 环境变量定制

const child = spawn('echo', ['$ENV_VAR'], {
  env: { ...process.env, ENV_VAR: 'custom value' }
});

七、electron上验证测试方法

端到端测试 (Electron 环境)

  1. main.js
const { app, BrowserWindow, ipcMain, Tray, nativeImage } = require('electron');
const { spawn } = require('child_process');
const path = require('path');

let mainWindow, tray;

function createWindow() {
    tray = new Tray(nativeImage.createFromPath(path.join(__dirname, 'electron_white.png')));
    mainWindow = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            nodeIntegration: false,
            contextIsolation: true,
            preload: path.join(__dirname, 'preload.js')
        }
    });
    console.log('electron-demo start');
    mainWindow.loadFile('index.html');
}

app.whenReady().then(() => {
    createWindow();

    app.on('activate',() => {
        if(BrowserWindow.getAllWindow().length === 0) createWindow();
    });

    app.on('window-all-closed', () => {
        if (process.platform !== 'darwin') app.quit();
    });
});

// 监听渲染进程的触发请求
ipcMain.on('run-script', (event, arg) => {
    // 示例:运行一个简单的 ping 命令(跨平台)
    const child = spawn('ping', ['-c', '4', 'www.baidu.com']);

    // 监听子进程输出
    child.stdout.on('data', (data) => {
        // 将数据发送到渲染进程
        mainWindow.webContents.send('script-output', data.toString());
    });

    child.stderr.on('data', (data) => {
        mainWindow.webContents.send('script-error', data.toString());
    });

    child.on('close', (code) => {
        mainWindow.webContents.send('script-output', `子进程退出,退出码: ${code}`);
    });
});
  1. preload.js
const { contextBridge, ipcRenderer } = require('electron');

   contextBridge.exposeInMainWorld('electronAPI', {
       runScript: () => ipcRenderer.send('run-script'),
       onScriptOutput: (callback) => ipcRenderer.on('script-output', (_event, value) => callback(value)),
       onScriptError: (callback) => ipcRenderer.on('script-error', (_event, value) => callback(value))
   });
  1. renderer.js
// renderer.js
const startBtn = document.getElementById('start');
const output = document.getElementById('output');

startBtn.addEventListener('click', () => {
    window.electronAPI.runScript();
});

// 接收主进程消息
window.electronAPI.onScriptOutput((data) => {
    output.innerText += data;
});

window.electronAPI.onScriptError((data) => {
    output.innerText += `错误: ${data}`;
});
  1. index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Electron Child Process Demo</title>
</head>
<body>
<h1>Electron Child Process Demo</h1>
<p>查看控制台输出以了解主进程和子进程的通信情况。</p>
<button id="start">运行脚本</button>
<pre id="output"></pre>
<script src="renderer.js"></script>
</body>
</html>
  1. package.json
{
  "name": "electron-example",
  "version": "1.0.0",
  "description": "Electron simple example",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  }
}
  1. 运行效果
  • 启动后界面展示如下

    image

  • 点击运行脚本按钮后

image

八、常见问题解决方案

  1. 命令找不到错误

    • 确保命令在系统 PATH 中
    • 或使用绝对路径
    • 或在选项中指定 shell: true
  2. 跨平台兼容性问题

    const cmd = process.platform === 'win32' ? 'dir' : 'ls';
    const args = process.platform === 'win32' ? ['/w'] : ['-lh'];
    const child = spawn(cmd, args);
    
  3. 进程不退出问题

    • 确保正确处理了所有流
    • 必要时调用 child.kill()
  4. 性能优化

    • 对大输出使用流处理
    • 避免频繁创建/销毁进程,考虑进程池

九、与 Electron 集成的注意事项

  1. 渲染进程中使用

    // 在渲染进程中
    const { ipcRenderer } = require('electron');
    
    ipcRenderer.invoke('spawn-command', 'ls', ['-lh'])
      .then(output => console.log(output));
    
    // 在主进程中处理
    ipcMain.handle('spawn-command', async (event, cmd, args) => {
      return new Promise((resolve, reject) => {
        const child = spawn(cmd, args);
        let output = '';
    
        child.stdout.on('data', (data) => output += data.toString());
        child.on('close', () => resolve(output));
        child.on('error', reject);
      });
    });
    
  2. 安全考虑

    • 永远不要直接将用户输入传递给 spawn()
    • 使用白名单验证命令和参数
    • 考虑使用 execFile 代替,除非需要 shell 特性
  3. 打包后路径问题

    • 使用 app.getPath() 获取正确路径
    • 对于打包的二进制文件,使用 extraResources 配置
const { app } = require('electron');
const path = require('path');

function getExecutablePath(name) {
    if (app.isPackaged) {
        // 打包后的路径结构
       return process.platform === 'win32'
          ? path.join(process.resourcesPath, 'bin', `${name}.exe`)
          : path.join(process.resourcesPath, 'bin', name);
    }

    // 开发环境路径 
    return path.join(__dirname, 'bin', name);
}

// 使用示例
const child = spawn(getExecutablePath('my-tool'), ['--arg=value']);

通过以上详细介绍和示例,你应该能够全面掌握 Electron 中 child_process.spawn() 的使用方法,并能在实际项目中灵活应用。


返回子进程目录 | 返回文档首页