在 Node 中使用 execa 運(yùn)行命令

Node.js 有一個內(nèi)置 child_process 模塊。它提供了一些方法,允許我們編寫在子進(jìn)程中運(yùn)行命令的腳本。

這些命令可以運(yùn)行安裝在我們運(yùn)行腳本的機(jī)器上的任何程序。

什么是 execa?

execachild_process 模塊提供了一個包裝器,以便于使用。

本文將介紹 execa 的好處,以及如何使用它。

使用 execa 的好處

execa 相對于內(nèi)置的 Node.js child_process 模塊有幾個好處。

首先,execa 公開了一個基于 promise 的 API。這意味著我們可以在代碼中使用 async/await,而不需要像使用異步 child_process 模塊方法那樣創(chuàng)建回調(diào)函數(shù)。如果我們需要它,還有一個 execaSync 方法可以同步運(yùn)行命令。

execa 還可以方便地轉(zhuǎn)義并引用我們傳遞給它的任何命令參數(shù)。例如,如果我們傳遞一個帶有空格或引號的字符串,execa 將為我們處理轉(zhuǎn)義。

程序在輸出的末尾添加一行新行是很常見的。這有利于命令行的可讀性,但在腳本上下文中沒有幫助,因此默認(rèn)情況下,execa 會自動為我們刪除這些新行。

如果執(zhí)行腳本(node)的進(jìn)程因任何原因死掉,我們可能不希望子進(jìn)程掛起。execa 會自動為我們清理子進(jìn)程,確保我們不會出現(xiàn)“僵尸”進(jìn)程。

使用 execa 的另一個好處是,它支持在 Windows 上使用 Node.js 運(yùn)行子進(jìn)程。它使用了流行的 cross-spawn 包,該包可以解決 Node.js 的已知問題。

開始使用 execa

創(chuàng)建并切換到 tutorial 目錄:

$ mkdir execa-test
$ cd execa-test

初始化項(xiàng)目:

$ npm init -y

安裝 execa

$ npm i execa

在 Node.js 中使用純 ES 模塊包

Node.js 支持兩種不同的模塊類型:

  • CommonJS 模塊(CJS):使用 module.exports 以導(dǎo)出函數(shù)和對象,并 require() 將其加載到另一個模塊中
  • ECMAScript 模塊(ESM):使用 export 導(dǎo)出函數(shù)和對象,并使用 import 將它們加載到另一個模塊中

execa 在其 v6.0.0 版本成為一個純 ESM 包。這意味著我們必須使用支持 ES 模塊的 Node.js 版本才能使用這個包。

對于我們自己的代碼,我們可以顯示 Node.js 通過在 package.json 中添加 "type": "module",確保我們項(xiàng)目中的所有模塊都是 ES 模塊?;蛘呶覀兛梢詫蝹€腳本的文件擴(kuò)展名設(shè)置為 .mjs

打開 package.json 文件,添加:

{
  "type": "module"
}

現(xiàn)在,我們可以在創(chuàng)建的任何腳本中從 execa 包導(dǎo)入 execa 方法:

import { execa } from 'execa'

盡管 ES 模塊在 Node.js 包和應(yīng)用程序中得到采用,但 CommonJS 模塊仍然是使用最廣泛的模塊類型。如果由于任何原因您無法在代碼庫中使用 ES 模塊,則需要 import 在 CommonJS 模塊中使用該方法:

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

  // ...
}

run()

注意,我們需要將對 import 函數(shù)的調(diào)用包裝在一個 async 函數(shù)中,因?yàn)?CommonJS 模塊不支持頂級 await。

詳細(xì)內(nèi)容可以閱讀阮一峰老師的 Node.js 如何處理 ES6 模塊。

使用 execa 運(yùn)行命令

現(xiàn)在,我們將創(chuàng)建一個腳本,使用 execa 運(yùn)行以下命令:

$ echo "Process execution for humans"

echo 程序打印出傳遞給它的文本字符串。

創(chuàng)建 run.js 文件,編寫如下內(nèi)容:

import { execa } from 'execa'

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

運(yùn)行文件:

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

stdout 是什么?

程序可以訪問“標(biāo)準(zhǔn)流”。本文使用了其中兩個:

  • stdout — 標(biāo)準(zhǔn)輸出是程序?qū)⑵漭敵鰯?shù)據(jù)寫入的流
  • stderr — 標(biāo)準(zhǔn)錯誤是程序?qū)懭脲e誤消息并調(diào)試數(shù)據(jù)的流

當(dāng)我們運(yùn)行程序時,這些標(biāo)準(zhǔn)流通常連接到父進(jìn)程。如果我們在終端上運(yùn)行一個程序,這意味著終端將接收并顯示程序發(fā)送到 stdoutstderr 流的數(shù)據(jù)。

當(dāng)我們運(yùn)行本文中的腳本時,子進(jìn)程的 stdoutstderr 流連接到父進(jìn)程 node,允許我們訪問子進(jìn)程發(fā)送給它們的任何數(shù)據(jù)。

捕獲 stderr

調(diào)用 execa 方法從對象解構(gòu) 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: '' }

再次運(yùn)行后,您會發(fā)現(xiàn) stderr 的值是一個空字符串('')。這是因?yàn)?echo 命令沒有向標(biāo)準(zhǔn)錯誤流寫入任何數(shù)據(jù)。

ls(1) 程序列出有關(guān)文件和目錄的信息。如果文件不存在,它將向標(biāo)準(zhǔn)錯誤流寫入錯誤消息。

替換代碼:

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

運(yùn)行后,將輸出以下內(nèi)容:

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 方法運(yùn)行此命令時引發(fā)錯誤。這是因?yàn)樽舆M(jìn)程的退出代碼不是 0。退出代碼為 0 通常表示進(jìn)程已成功運(yùn)行,而任何其他值都表示存在問題。

execa 方法拋出的錯誤對象包含一個 stderr 屬性,其中包含由 ls 寫入標(biāo)準(zhǔn)錯誤流的數(shù)據(jù)。

我們現(xiàn)在需要實(shí)現(xiàn)錯誤處理,這樣如果命令失敗,我們就可以優(yōu)雅地處理它,而不是讓它破壞我們的腳本。

注意:程序通常會成功運(yùn)行,但也會將調(diào)試消息寫入 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})`
  )
}

運(yùn)行腳本后,輸出:

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

取消子進(jìn)程

一旦我們開始執(zhí)行一個命令,我們可能想要取消這個過程,例如,如果它需要比預(yù)期更長的時間才能完成。execa 提供了一個 cancel 方法,我們可以調(diào)用它向子進(jìn)程發(fā)送 SIGTERM 信號。

// 創(chuàng)建一個僅運(yùn)行 5s 的子進(jìn)程
// 注意:這里不需要加 await,這里演示 1s 后取消子進(jìn)程
const subprocess = execa('sleep', ['5s'])

// 1s 后取消子進(jìn)程
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)
  }
}

當(dāng) subprocess.cancel 方法被調(diào)用時,錯誤對象上的 isCanceled 屬性被設(shè)置為true。這使我們能夠確定錯誤的原因并顯示特定的錯誤消息。

運(yùn)行 node run.js,輸出:

ERROR: The command took too long to run.

如果我們需要強(qiáng)制一個子進(jìn)程結(jié)束,我們可以調(diào)用 subprocess.kill 方法而不是 subprocess.cancel。

使用 execa 從子進(jìn)程管道輸出

execa 方法返回的對象中的 stdoutstderr 屬性是可讀的流。我們已經(jīng)看到了如何將發(fā)送到這些流的數(shù)據(jù)保存在變量中。例如,我們可以通過管道將可讀流轉(zhuǎn)換為可寫流,以便在子進(jìn)程運(yùn)行時查看其輸出。

去除定時器,更改子進(jìn)程的代碼,然后,我們將通過管道將 stdout 流從子進(jìn)程傳輸?shù)礁高M(jìn)程的 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: '' }

將輸出重定向到文件

我們可以將子進(jìn)程的 stdout 數(shù)據(jù)寫入一個文件,而不是通過管道傳輸?shù)礁高M(jìn)程的 stdout 流。

首先,導(dǎo)入內(nèi)置 fs 模塊:

import fs from 'fs'

然后,我們可以替換對 pipe 方法的現(xiàn)有調(diào)用:

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

這將創(chuàng)建一個 fs.WriteStream 實(shí)例,其中包含來自子流程的數(shù)據(jù)。stdout 可讀流將通過管道傳輸?shù)健?/p>

運(yùn)行腳本,輸出:

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

我們還應(yīng)該看到一個名為 stdout.txt 的文件,其中包含來自子進(jìn)程標(biāo)準(zhǔn)輸出流的數(shù)據(jù):

$ cat stdout.txt
is it me you're looking for?
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容