深入Node.js的程序與子程序:從文件到實踐
歡迎關注Github倉庫,這是一個自2018年起持續更新的前端&演算法開源部落格。目前已有node學習、js面試筆記、css3動畫設計、webpack4系列教程、設計模式、劍指offer·js版等多個系列。
倉庫地址:https://github.com/dongyuanxin/blog
程序:process模組
process 模組是 nodejs 提供給開發者用來和當前程序互動的工具,它的提供了很多實用的 API。從文件出發,管中窺豹,進一步認識和學習 process 模組:
如何處理命令引數? 如何處理工作目錄? 如何處理異常? 如何處理程序退出? process 的標準流物件 深入理解 process.nextTick
如何處理命令引數?
命令列引數指的是 2 個方面:
傳給 node 的引數。例如 node --harmony script.js --version
中,--harmony
就是傳給 node 的引數傳給程序的引數。例如 node script.js --version --help
中,--version --help
就是傳給程序的引數
它們分別通過 process.argv
和 process.execArgv
來獲得。
如何處理工作目錄?
通過process.cwd()
可以獲取當前的工作目錄。
通過process.chdir(directory)
可以切換當前的工作目錄,失敗後會丟擲異常。實踐如下:
function safeChdir(dir) {
try {
process.chdir(dir);
return true;
} catch (error) {
return false;
}
}
如何處理異常?
uncaughtException 事件
Nodejs 可以通過 try-catch 來捕獲異常。如果異常未捕獲,則會一直從底向事件迴圈冒泡。如是冒泡到事件迴圈的異常沒被處理,那麼就會導致當前程序異常退出。
根據文件,可以通過監聽 process 的 uncaughtException 事件,來處理未捕獲的異常:
process.on("uncaughtException", (err, origin) => {
console.log(err.message);
});
const a = 1 / b;
console.log("abc"); // 不會執行
上面的程式碼,控制檯的輸出是:b is not defined
。捕獲了錯誤資訊,並且程序以0
退出。開發者可以在 uncaughtException 事件中,清除一些已經分配的資源(檔案描述符、控制代碼等),不推薦在其中重啟程序。
unhandledRejection 事件
如果一個 Promise 回撥的異常沒有被.catch()
捕獲,那麼就會觸發 process 的 unhandledRejection 事件:
process.on("unhandledRejection", (err, promise) => {
console.log(err.message);
});
Promise.reject(new Error("錯誤資訊")); // 未被catch捕獲的異常,交由unhandledRejection事件處理
warning 事件
告警不是 Node.js 和 Javascript 錯誤處理流程的正式組成部分。 一旦探測到可能導致應用效能問題,缺陷或安全隱患相關的程式碼實踐,Node.js 就可發出告警。
比如前一段程式碼中,如果出現未被捕獲的 promise 回撥的異常,那麼就會觸發 warning 事件。
如何處理程序退出?
process.exit() vs process.exitCode
一個 nodejs 程序,可以通過 process.exit() 來指定退出程式碼,直接退出。不推薦直接使用 process.exit(),這會導致事件迴圈中的任務直接不被處理,以及可能導致資料的截斷和丟失(例如 stdout 的寫入)。
setTimeout(() => {
console.log("我不會執行");
});
process.exit(0);
正確安全的處理是,設定 process.exitCode,並允許程序自然退出。
setTimeout(() => {
console.log("我不會執行");
});
process.exitCode = 1;
beforeExit 事件
用於處理程序退出的事件有:beforeExit 事件 和 exit 事件。
當 Node.js 清空其事件迴圈並且沒有其他工作要安排時,會觸發 beforeExit 事件。例如在退出前需要一些非同步操作,那麼可以寫在 beforeExit 事件中:
let hasSend = false;
process.on("beforeExit", () => {
if (hasSend) return; // 避免死迴圈
setTimeout(() => {
console.log("mock send data to serve");
hasSend = true;
}, 500);
});
console.log(".......");
// 輸出:
// .......
// mock send data to serve
注意:在 beforeExit 事件中如果是非同步任務,那麼又會被新增到任務佇列。此時,任務佇列完成所有任務後,又回觸發 beforeExit 事件。因此,不處理的話,可能出現死迴圈的情況。如果是顯式呼叫 exit(),那麼不會觸發此事件。
exit 事件
在 exit 事件中,只能執行同步操作。在呼叫 'exit' 事件監聽器之後,Node.js 程序將立即退出,從而導致在事件迴圈中仍排隊的任何其他工作被放棄。
process 的標準流物件
process 提供了 3 個標準流。需要注意的是,它們有些在某些時候是同步阻塞的(請見文件)。
process.stderr:WriteStream 型別, console.error
的底層實現,預設對應螢幕process.stdout:WriteStream 型別, console.log
的底層實現,預設對應螢幕process.stdin:ReadStream 型別,預設對應鍵盤輸入
下面是基於“生產者-消費者模型”的讀取控制檯輸入並且及時輸出的程式碼:
process.stdin.setEncoding("utf8");
process.stdin.on("readable", () => {
let chunk;
while ((chunk = process.stdin.read()) !== null) {
process.stdout.write(`>>> ${chunk}`);
}
});
process.stdin.on("end", () => {
process.stdout.write("結束");
});
關於事件的含義,還是請看stream 的文件。
深入理解 process.nextTick
我第一次看到 process.nextTick 的時候是比較懵的,看文件可以知道,它的用途是:把回撥函式作為微任務,放入事件迴圈的任務佇列中。但這麼做的意義是什麼呢?
因為 nodejs 並不適合計算密集型的應用,一個程序就一個執行緒,在當下時間點上,就一個事件在執行。那麼,如果我們的事件佔用了很多 cpu 時間,那麼之後的事件就要等待非常久。所以,nodejs 的一個程式設計原則是儘量縮短每一個事件的執行事件。process.nextTick 的作用就在這,將一個大的任務分解成多個小的任務。示例程式碼如下:
// 被拆分成2個函式執行
function BigThing() {
doPartThing();
process.nextTick(() => finishThing());
}
在事件迴圈中,何時執行 nextTick 註冊的任務呢?請看下面的程式碼:
setTimeout(function() {
console.log("第一個1秒");
process.nextTick(function() {
console.log("第一個1秒:nextTick");
});
}, 1000);
setTimeout(function() {
console.log("第2個1秒");
}, 1000);
console.log("我要輸出1");
process.nextTick(function() {
console.log("nextTick");
});
console.log("我要輸出2");
輸出的結果如下,nextTick 是早於 setTimeout:
我要輸出1
我要輸出2
nextTick
第一個1秒
第一個1秒:nextTick
第2個1秒
在瀏覽器端,nextTick 會退化成 setTimeout(callback, 0)
。但在 nodejs 中請使用 nextTick 而不是 setTimeout,前者效率更高,並且嚴格來說,兩者建立的事件在任務佇列中順序並不一樣(請看前面的程式碼)。
子程序:child_process模組
掌握 nodejs 的 child_process 模組能夠極大提高 nodejs 的開發能力,例如主從程序來優化 CPU 計算的問題,多程序開發等等。本文從以下幾個方面介紹 child_process 模組的使用:
建立子程序 父子程序通訊 獨立子程序 程序管道
建立子程序
nodejs 的 child_process 模組建立子程序的方法:spawn, fork, exec, execFile。它們的關係如下:
fork, exec, execFile 都是通過 spawn 來實現的。 exec 預設會建立 shell。execFile 預設不會建立 shell,意味著不能使用 I/O 重定向、file glob,但效率更高。 spawn、exec、execFile 都有同步版本,可能會造成程序阻塞。
child_process.spawn()
的使用:
const { spawn } = require("child_process");
// 返回ChildProcess物件,預設情況下其上的stdio不為null
const ls = spawn("ls", ["-lh"]);
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}`);
});
child_process.exec()
的使用:
const { exec } = require("child_process");
// 通過回撥函式來操作stdio
exec("ls -lh", (err, stdout, stderr) => {
if (err) {
console.error(`執行的錯誤: ${err}`);
return;
}
console.log(`stdout: ${stdout}`);
console.error(`stderr: ${stderr}`);
});
父子程序通訊
fork()
返回的 ChildProcess 物件,監聽其上的 message 事件,來接受子程序訊息;呼叫 send 方法,來實現 IPC。
parent.js 程式碼如下:
const { fork } = require("child_process");
const cp = fork("./sub.js");
cp.on("message", msg => {
console.log("父程序收到訊息:", msg);
});
cp.send("我是父程序");
sub.js 程式碼如下:
process.on("message", m => {
console.log("子程序收到訊息:", m);
});
process.send("我是子程序");
執行後結果:
父程序收到訊息: 我是子程序
子程序收到訊息: 我是父程序
獨立子程序
在正常情況下,父程序一定會等待子程序退出後,才退出。如果想讓父程序先退出,不受到子程序的影響,那麼應該:
呼叫 ChildProcess 物件上的 unref()
options.detached
設定為 true子程序的 stdio 不能是連線到父程序
main.js 程式碼如下:
const { spawn } = require("child_process");
const subprocess = spawn(process.argv0, ["sub.js"], {
detached: true,
stdio: "ignore"
});
subprocess.unref();
sub.js 程式碼如下:
setInterval(() => {}, 1000);
程序管道
options.stdio 選項用於配置在父程序和子程序之間建立的管道。 預設情況下,子程序的 stdin、 stdout 和 stderr 會被重定向到 ChildProcess 物件上相應的 subprocess.stdin、subprocess.stdout 和 subprocess.stderr 流。 這意味著可以通過監聽其上的 data
事件,在父程序中獲取子程序的 I/O 。
可以用來實現“重定向”:
const fs = require("fs");
const child_process = require("child_process");
const subprocess = child_process.spawn("ls", {
stdio: [
0, // 使用父程序的 stdin 用於子程序。
"pipe", // 把子程序的 stdout 通過管道傳到父程序 。
fs.openSync("err.out", "w") // 把子程序的 stderr 定向到一個檔案。
]
});
也可以用來實現"管道運算子":
const { spawn } = require("child_process");
const ps = spawn("ps", ["ax"]);
const grep = spawn("grep", ["ssh"]);
ps.stdout.on("data", data => {
grep.stdin.write(data);
});
ps.stderr.on("data", err => {
console.error(`ps stderr: ${err}`);
});
ps.on("close", code => {
if (code !== 0) {
console.log(`ps 程序退出,退出碼 ${code}`);
}
grep.stdin.end();
});
grep.stdout.on("data", data => {
console.log(data.toString());
});
grep.stderr.on("data", data => {
console.error(`grep stderr: ${data}`);
});
grep.on("close", code => {
if (code !== 0) {
console.log(`grep 程序退出,退出碼 ${code}`);
}
});
參考連結
Nodejs v12 Stream 文件 Nodejs v12 process 文件 nodejs 學習筆記 一篇文章構建你的 NodeJS 知識體系 Node.js - 程序學習筆記 glob Nodejs 進階:如何玩轉子程序(child_process)
放在最後
覺得不錯,幫忙點個讚唄,您的支援是對我最大的激勵 歡迎我的公眾號:「心譚部落格」,只專注於前端 + 演算法的原創分享