all
You must replace the baseURL in hugo.toml file when deploying, you can manage this announcement from the params.toml file.

Child process API: spawn vs exec

内容列表

利用 Node.js 编写一些命令行工具、一次性脚本是很方便的,而在这类场景下 child_process API 的 spawnexec 方法的应用则非常常见。在我使用它们时,却不知道该如何进行选择,遂对此进行了探究。

Child process API

先来看看 child_process API,根据官方文档描述:

The child_process module provides the ability to spawn subprocesses in a manner that is similar, but not identical, to popen(3). This capability is primarily provided by the child_process.spawn() function:

其类似于 Linux 的 popen 命令行为,spawn 是其核心方法,通过创建一个管道(pipe),调用 fork 生成一个子进程,并执行 shell 命令。例如,通过该 API 就可以以编程的方式生成子进程并执行二进制文件,这在编写脚本工具时是一个非常常见的场景。

在这里,主要讨论的是异步版本,当然 Node.js 为它们提供了相应的同步版本,例如 spawnSyncexecSync

spawn

前面说到 spawn 是 Child process API 的核心方法,其实从源码可以一窥究竟:

// https://github.com/nodejs/node/blob/v16.8.0/lib/child_process.js
function exec(command, options, callback) {
  const opts = normalizeExecArgs(command, options, callback);
  return module.exports.execFile(opts.file, opts.options, opts.callback);
}

function execFile(file /* , args, options, callback */) {
  // ...
  const child = spawn();
  // ...
}

// ---
function fork(modulePath /* , args, options */) {
  // ...
  return spawn(options.execPath, args, options);
}

可见,execfork 最终还是依赖于 spawn 的实现。而对于后者的实现:

const child_process = require('internal/child_process');
const { ChildProcess } = child_process;

function spawn(file, args, options) {
  // ...
  const child = new ChildProcess();
  child.spawn(options);
  // ...
}

依赖于底层的内部模块 internal/child_process

spawn 的主要功能是生成一个子进程,并执行给定的命令,父子进程之间通过管道(pipe)传递 stdio 信息,而且默认不生成 shell。根据示例:

const { spawn } = require('child_process');
const ls = spawn('ls', ['-lh', '/usr']);

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

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

ls.on('close', (code) => {
  console.log(`child process exited with code ${code}`);
});

父进程通过监听子进程相应的 stdio 事件进行通信。

exec

前面根据源码可以看到 exec 的实现基于 spawn,但不同的是,前者在生成子进程的同时,会先生成一个 shell,然后在 shell 中执行给定的命令,子进程的输出信息会进行缓冲并最终传递给回调函数。根据示例:

const { exec } = require('child_process');
exec('cat *.js missing_file | wc -l', (error, stdout, stderr) => {
  if (error) {
    console.error(`exec error: ${error}`);
    return;
  }
  console.log(`stdout: ${stdout}`);
  console.error(`stderr: ${stderr}`);
});

父进程并非通过监听子进程的 stdio 事件,而是给子进程传递一个回调函数来获取子进程的输出信息。

这里有一个显著的区别,exec 会先生成一个 shell 在执行命令,而 spawn 则会直接执行命令,但考虑到前者基于后者实现,事实上后者可以通过传递 options.shell 选项来选择是否生成 shell。

官方文档有一句话也值得留意:

Unlike the exec(3) POSIX system call, child_process.exec() does not replace the existing process and uses a shell to execute the command.

exec 的 POSIX 系统调用的行为是,在当前进程中用新的进程映像(程序)替换旧的进程映像并执行,本质上并没有生成新的进程,也就不存在父子进程的概念。而在这里,Node.js 的 exec 方法的行为并不是替换进程映射,而是生成 shell 去执行命令。

spawn vs exec

现在可以总结一下两者的显著区别:

  • spawn 默认不生成 shell,而 exec 必然会生成一个 shell
  • spawn 通过 stdio 事件流和父进程通信,而 exec 会对输出信息进行缓冲并通过回调函数将其传递给父进程,且后者默认有 1024 * 1024 字节的缓冲区限制

对于第一点,如果要执行的命令依赖于 shell 的一些功能,比如管道、I/O 重定向则选择 exec 会更便捷。对于第二点,对比示例代码,可以很明显的看出来,spawn 适合长时间执行的命令,且有持续的输出信息;而后者更适合执行短时的命令,且在命令执行完后一次性获取输出结果。

工具库 execa

分析完它们两者的区别之后,这里推荐一个 npm 工具包 execa,其对 child_process 的方法进行了扩展和抽象,在很多常见的使用场景中大大减少了模板代码,也为调试提供了一定的便利性。看看文档中一段示例代码:

const execa = require('execa');

(async () => {
  // Catching an error
  try {
    await execa('unknown', ['command']);
  } catch (error) {
    console.log(error);
    /*
		{
			message: 'Command failed with ENOENT: unknown command spawn unknown ENOENT',
			errno: -2,
			code: 'ENOENT',
			syscall: 'spawn unknown',
			path: 'unknown',
			spawnargs: ['command'],
			originalMessage: 'spawn unknown ENOENT',
			shortMessage: 'Command failed with ENOENT: unknown command spawn unknown ENOENT',
			command: 'unknown command',
			escapedCommand: 'unknown command',
			stdout: '',
			stderr: '',
			all: '',
			failed: true,
			timedOut: false,
			isCanceled: false,
			killed: false
		}
		*/
  }
})();

上面的代码中,错误信息对于开发者来说是易读的,调试起来难度要小很多。

更多的东西,建议直接看该 npm 包的文档进行详细了解。

参考资源

comments powered by Disqus

相关

前端工程化:对于构建工具链的简单思考

前端工程化是在做与业务开发完全不同的事情,旨在解决软件工程领域与开发者密切相关的问题,通常会将其与基建开发、DevOps 放在一起讨论。前端开发是复杂的,其结合了 HTML/CSS/JavaScript 3 种语言,甚至还有很多其超集,没有开箱即用的工具链,不像 Java Web 开发、Android 开发等等有官方或者商业领域非常成熟的工具可以利用,一切都源于开源社区的从 0 开始构建。正因如此,前端工程化领域百花齐放,开放与创新展现的淋漓尽致,这也是前端开发者了解学习软件工程的机会。

了解更多

网络通信关键概念

计算机网络是通过通信设备与线路将地理上分散并且具有独立功能的计算机系统连接在一起,并由功能完善的软件来控制,进而实现资源共享的系统。从物理组成上来看,计算机网络包括硬件、软件和协议三大部分。计算机网络中结点间相互通信是由控制信息传送的网络协议及其他相应的网络软件共同实现的。在计算机网络通信中,有部分关键性概念需要理解透彻,在此做一总结。

了解更多

DOM-事件

JavaScript 的作用就是让 html 静态页面具备动态效果,而这些基本都是利用 DOM 事件来实现的。

了解更多