Node.js 有一個內(nèi)置 child_process 模塊。它提供了一些方法,允許我們編寫在子進(jìn)程中運(yùn)行命令的腳本。
這些命令可以運(yùn)行安裝在我們運(yùn)行腳本的機(jī)器上的任何程序。
什么是 execa?
execa 為 child_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ā)送到 stdout 和 stderr 流的數(shù)據(jù)。
當(dāng)我們運(yùn)行本文中的腳本時,子進(jìn)程的 stdout 和 stderr 流連接到父進(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 方法返回的對象中的 stdout 和 stderr 屬性是可讀的流。我們已經(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?