美文网首页前端大杂烩
在 Node 中使用 execa 运行命令

在 Node 中使用 execa 运行命令

作者: lio_zero | 来源:发表于2022-05-26 14:28 被阅读0次

    Node.js 有一个内置 child_process 模块。它提供了一些方法,允许我们编写在子进程中运行命令的脚本。

    这些命令可以运行安装在我们运行脚本的机器上的任何程序。

    什么是 execa?

    execachild_process 模块提供了一个包装器,以便于使用。

    本文将介绍 execa 的好处,以及如何使用它。

    使用 execa 的好处

    execa 相对于内置的 Node.js child_process 模块有几个好处。

    首先,execa 公开了一个基于 promise 的 API。这意味着我们可以在代码中使用 async/await,而不需要像使用异步 child_process 模块方法那样创建回调函数。如果我们需要它,还有一个 execaSync 方法可以同步运行命令。

    execa 还可以方便地转义并引用我们传递给它的任何命令参数。例如,如果我们传递一个带有空格或引号的字符串,execa 将为我们处理转义。

    程序在输出的末尾添加一行新行是很常见的。这有利于命令行的可读性,但在脚本上下文中没有帮助,因此默认情况下,execa 会自动为我们删除这些新行。

    如果执行脚本(node)的进程因任何原因死掉,我们可能不希望子进程挂起。execa 会自动为我们清理子进程,确保我们不会出现“僵尸”进程。

    使用 execa 的另一个好处是,它支持在 Windows 上使用 Node.js 运行子进程。它使用了流行的 cross-spawn 包,该包可以解决 Node.js 的已知问题。

    开始使用 execa

    创建并切换到 tutorial 目录:

    $ mkdir execa-test
    $ cd execa-test
    

    初始化项目:

    $ npm init -y
    

    安装 execa

    $ npm i execa
    

    在 Node.js 中使用纯 ES 模块包

    Node.js 支持两种不同的模块类型:

    • CommonJS 模块(CJS):使用 module.exports 以导出函数和对象,并 require() 将其加载到另一个模块中
    • ECMAScript 模块(ESM):使用 export 导出函数和对象,并使用 import 将它们加载到另一个模块中

    execa 在其 v6.0.0 版本成为一个纯 ESM 包。这意味着我们必须使用支持 ES 模块的 Node.js 版本才能使用这个包。

    对于我们自己的代码,我们可以显示 Node.js 通过在 package.json 中添加 "type": "module",确保我们项目中的所有模块都是 ES 模块。或者我们可以将单个脚本的文件扩展名设置为 .mjs

    打开 package.json 文件,添加:

    {
      "type": "module"
    }
    

    现在,我们可以在创建的任何脚本中从 execa 包导入 execa 方法:

    import { execa } from 'execa'
    

    尽管 ES 模块在 Node.js 包和应用程序中得到采用,但 CommonJS 模块仍然是使用最广泛的模块类型。如果由于任何原因您无法在代码库中使用 ES 模块,则需要 import 在 CommonJS 模块中使用该方法:

    async function run() {
      const { execa } = await import('execa')
    
      // ...
    }
    
    run()
    

    注意,我们需要将对 import 函数的调用包装在一个 async 函数中,因为 CommonJS 模块不支持顶级 await

    详细内容可以阅读阮一峰老师的 Node.js 如何处理 ES6 模块

    使用 execa 运行命令

    现在,我们将创建一个脚本,使用 execa 运行以下命令:

    $ echo "Process execution for humans"
    

    echo 程序打印出传递给它的文本字符串。

    创建 run.js 文件,编写如下内容:

    import { execa } from 'execa'
    
    // execa 返回的 promise 与对象解析。
    const { stdout } = await execa('echo', ['Process execution for humans'])
    console.log({ stdout })
    

    运行文件:

    $ node run.js
    { stdout: '"Process execution for humans"' }
    

    stdout 是什么?

    程序可以访问“标准流”。本文使用了其中两个:

    • stdout — 标准输出是程序将其输出数据写入的流
    • stderr — 标准错误是程序写入错误消息并调试数据的流

    当我们运行程序时,这些标准流通常连接到父进程。如果我们在终端上运行一个程序,这意味着终端将接收并显示程序发送到 stdoutstderr 流的数据。

    当我们运行本文中的脚本时,子进程的 stdoutstderr 流连接到父进程 node,允许我们访问子进程发送给它们的任何数据。

    捕获 stderr

    调用 execa 方法从对象解构 stderr 属性:

    import { execa } from 'execa'
    
    const { stdout, stderr } = await execa('echo', ['Process execution for humans'])
    console.log({ stdout, stderr }) // { stdout: 'Process execution for humans', stderr: '' }
    

    再次运行后,您会发现 stderr 的值是一个空字符串('')。这是因为 echo 命令没有向标准错误流写入任何数据。

    ls(1) 程序列出有关文件和目录的信息。如果文件不存在,它将向标准错误流写入错误消息。

    替换代码:

    const { stdout, stderr } = await execa('ls', ['file.txt'])
    

    运行后,将输出以下内容:

    Error: Command failed with exit code 1: ls file.txt
    ...
    {
      shortMessage: 'Command failed with exit code 2: ls file.txt',
      command: 'ls file.txt',
      escapedCommand: 'ls file.txt',
      exitCode: 1,
      // ...
    }
    

    使用 execa 方法运行此命令时引发错误。这是因为子进程的退出代码不是 0。退出代码为 0 通常表示进程已成功运行,而任何其他值都表示存在问题。

    execa 方法抛出的错误对象包含一个 stderr 属性,其中包含由 ls 写入标准错误流的数据。

    我们现在需要实现错误处理,这样如果命令失败,我们就可以优雅地处理它,而不是让它破坏我们的脚本。

    注意:程序通常会成功运行,但也会将调试消息写入 stderr

    execa 中的错误处理

    我们可以试着把命令包装 try...catch 语句并输出自定义错误消息,如下所示:

    try {
      const { stdout, stderr } = await execa('ls', ['file.txt'])
    
      console.log({ stdout, stderr })
    } catch (error) {
      console.error(
        `ERROR: The command failed. stderr: ${error.stderr} (${error.exitCode})`
      )
    }
    

    运行脚本后,输出:

    ERROR: The command failed. stderr: ls: cannot access 'file.txt': No such file or directory (1)
    

    取消子进程

    一旦我们开始执行一个命令,我们可能想要取消这个过程,例如,如果它需要比预期更长的时间才能完成。execa 提供了一个 cancel 方法,我们可以调用它向子进程发送 SIGTERM 信号。

    // 创建一个仅运行 5s 的子进程
    // 注意:这里不需要加 await,这里演示 1s 后取消子进程
    const subprocess = execa('sleep', ['5s'])
    
    // 1s 后取消子进程
    setTimeout(() => {
      subprocess.cancel()
    }, 1000)
    
    try {
      const { stdout, stderr } = await subprocess
    
      console.log({ stdout, stderr })
    } catch (error) {
      if (error.isCanceled) {
        console.error(`ERROR: The command took too long to run.`)
      } else {
        console.error(error)
      }
    }
    

    subprocess.cancel 方法被调用时,错误对象上的 isCanceled 属性被设置为true。这使我们能够确定错误的原因并显示特定的错误消息。

    运行 node run.js,输出:

    ERROR: The command took too long to run.
    

    如果我们需要强制一个子进程结束,我们可以调用 subprocess.kill 方法而不是 subprocess.cancel

    使用 execa 从子进程管道输出

    execa 方法返回的对象中的 stdoutstderr 属性是可读的流。我们已经看到了如何将发送到这些流的数据保存在变量中。例如,我们可以通过管道将可读流转换为可写流,以便在子进程运行时查看其输出。

    去除定时器,更改子进程的代码,然后,我们将通过管道将 stdout 流从子进程传输到父进程的 stdout 流:

    const subprocess = execa('echo', ["is it me you're looking for?"])
    
    subprocess.stdout.pipe(process.stdout)
    
    is it me you're looking for?
    { stdout: "is it me you're looking for?", stderr: '' }
    

    将输出重定向到文件

    我们可以将子进程的 stdout 数据写入一个文件,而不是通过管道传输到父进程的 stdout 流。

    首先,导入内置 fs 模块:

    import fs from 'fs'
    

    然后,我们可以替换对 pipe 方法的现有调用:

    subprocess.stdout.pipe(fs.createWriteStream('stdout.txt'))
    

    这将创建一个 fs.WriteStream 实例,其中包含来自子流程的数据。stdout 可读流将通过管道传输到。

    运行脚本,输出:

    { stdout: "is it me you're looking for?", stderr: '' }
    

    我们还应该看到一个名为 stdout.txt 的文件,其中包含来自子进程标准输出流的数据:

    $ cat stdout.txt
    is it me you're looking for?
    

    相关文章

      网友评论

        本文标题:在 Node 中使用 execa 运行命令

        本文链接:https://www.haomeiwen.com/subject/ahqcyrtx.html