Node.js 調(diào)試一路走來經(jīng)歷了什么
做為前端開發(fā),想必大家都寫過 Node.js 的代碼,也大概率用 debugger 斷點(diǎn)調(diào)試過。
我們可以用 Chrome Devtools 調(diào)試 Node.js 代碼,也可以用 VSCode 來調(diào)試它。調(diào)試工具是 Node.js 開發(fā)的基礎(chǔ)工具了。
但現(xiàn)在好用的調(diào)試工具也不是一開始就這樣的,它經(jīng)歷了一系列的演變過程。今天我們就來聊聊 Node.js 調(diào)試工具背后的故事吧。
相信還是有部分同學(xué)不知道 Node.js 代碼怎么調(diào)試的,所以我們先來過一遍怎么調(diào)試 Node.js 代碼:
調(diào)試 Node.js 代碼
準(zhǔn)備一段簡(jiǎn)單的 Node.js 代碼用來調(diào)試:
const os = require('os');
function func(a, b) {
return a + b;
}
console.log(func(1,2));
console.log(os.cpus());
它的邏輯就是執(zhí)行了一個(gè)加法,然后打印了 cpu 的核心的情況。
直接執(zhí)行是這樣的:
打印了 1 + 2 的結(jié)果,也就是 3 ,也打印了 CPU 核心的情況,8 核的 M1 芯片。
那怎么斷點(diǎn)調(diào)試呢?
執(zhí)行的時(shí)候加上一個(gè) --inspect 的參數(shù),就會(huì)啟動(dòng)調(diào)試服務(wù)了:
指定 --inspect-brk 參數(shù)還會(huì)在首行斷住。
可以看到啟動(dòng)了一個(gè) WebSocket 的服務(wù)端,這就是調(diào)試服務(wù),用某個(gè)調(diào)試工具客戶端連上就行了:
調(diào)試客戶端可以是 Chrome Devtools 也可以是 VSCode。
Chrome Devtools
比如用 Chrome Devtools 來連上是這樣的:
打開 chrome://inspect 的 url 就會(huì)看到這個(gè)可以連接的 target:
點(diǎn)擊 inspect 就是連上這個(gè) ws 服務(wù)端來做調(diào)試:
右邊可以看到調(diào)用棧、上下文的變量,可以單步執(zhí)行、可以打斷點(diǎn)等。
打印信息會(huì)輸出在 console:
VSCode
用 VSCode 調(diào)試的話需要在項(xiàng)目根目錄下加一個(gè) .vscode/launch.json 的文件,類型選擇 attach to process:
很容易理解,就是連接到目標(biāo)進(jìn)程的 ws 服務(wù)的意思:
端口是 9229,也就是我們調(diào)試服務(wù)啟動(dòng)的端口。
然后點(diǎn)擊調(diào)試面板的調(diào)試按鈕來啟動(dòng):
這樣也會(huì)在斷點(diǎn)處斷住,可以單步運(yùn)行、可以看調(diào)用棧、上下文的信息:
看到這里不知道有沒有同學(xué)會(huì)覺得這樣太麻煩了,每次都要起一個(gè) ws 調(diào)試服務(wù),然后再 attach,不能把這兩步合并到一塊自動(dòng)給做了嗎?
沒錯(cuò),確實(shí)可以合并到一塊,也就是起一個(gè) ws 服務(wù),然后自動(dòng) attach 上:
調(diào)試配置選擇 launch program:
只需要指定要調(diào)試的 Node.js 模塊的地址,然后點(diǎn)擊啟動(dòng),這樣就可以調(diào)試了:
注意,想達(dá)到和 --inspect-brk 一樣的首行斷住的效果,這里要執(zhí)行 stopOnEntry 為 true。
效果是一樣的:
這樣比直接啟動(dòng) ws 調(diào)試服務(wù),然后再 attach 還少了一步。
怎么調(diào)試 Node.js 講完了,大家是不是覺得這樣調(diào)試還是蠻方便的呢?
但其實(shí)最開始的調(diào)試并沒有這么好用,接下來我們看下之前的調(diào)試都是咋樣的吧:
Node.js Debugger 的歷史
從前面的實(shí)踐中我們也能發(fā)現(xiàn),調(diào)試的原理還是蠻清晰的:
啟動(dòng)一個(gè) WebSocket 服務(wù)端來提供各種運(yùn)行時(shí)的信息,這個(gè)服務(wù)是 JS Runtime 提供的,也就是 Node。
啟動(dòng)一個(gè) WebSocket 客戶端來實(shí)現(xiàn)調(diào)試的 UI,包括調(diào)用棧、上下文的顯示、打斷點(diǎn)、單步運(yùn)行等功能,比如我們用過的 Chrome Devtools、VSCode Debugger。
中間傳輸?shù)南⒕褪钦{(diào)試協(xié)議:
我們知道 Node.js 是基于 V8 的,V8 本身有調(diào)試協(xié)議 V8 Debug Protocol,所以 Node.js 最早的調(diào)試協(xié)議也就是 V8 Debug Protocol。
當(dāng)時(shí)調(diào)試是這樣的:
通過 node debug 來跑 js 文件,會(huì)在首行斷?。?/p>
然后可以通過 run、cont、next、step 等命令來實(shí)現(xiàn)單步調(diào)試,通過 backtrace 打印調(diào)用棧,通過 setBreakPoint 等設(shè)置斷點(diǎn):
比如用 setBreakPoint(sb)命令在第四行打個(gè)斷點(diǎn):
然后 cont(c) 命令繼續(xù)執(zhí)行,backtrace(bt) 打印調(diào)用棧:
雖然該有的調(diào)試功能都有,但是這樣調(diào)試還是比較費(fèi)勁的。
怎么能不用命令行調(diào)試,而是用 UI 來調(diào)試呢?
當(dāng)時(shí) Node 就瞄準(zhǔn)了 Chrome Devtools,它的調(diào)試 UI 就很不錯(cuò)。
但是 Chrome Devtools 的調(diào)試協(xié)議是 Chrome Devtools Protocol,和 V8 Debug Protocol 還是有些差距的,怎么能用上 Chrome Devtools 的調(diào)試工具來調(diào)試 Node 呢?
其實(shí)還挺容易想到的,就是加一個(gè)中間的服務(wù)來做轉(zhuǎn)換:
這個(gè)服務(wù)是 node-inspector 這個(gè)包提供的。
所以當(dāng)時(shí) node debug 服務(wù)跑起來之后,還要要再跑一個(gè) node-inspector 服務(wù),這樣才能用 chrome devtools 來調(diào)試 Node.js 代碼。
后來維護(hù) Node.js 的那些人覺得這樣也太麻煩了,要不讓 Node.js 提供的調(diào)試協(xié)議就直接就是兼容 Chrome Devtools Protocol 的吧。
當(dāng)時(shí)就有了這樣一個(gè) pr,把 v8 inspector 集成到 Node.js 中:
這個(gè) v8 inspector 就是從 chrome 的內(nèi)核 blink 里剝離出來的讓 v8 支持 chrome devtools protocol 的部分。
很明顯這需要 v8 團(tuán)隊(duì)的配合,所以說 Node.js 的發(fā)展還是很依賴 v8 團(tuán)隊(duì)的支持的。
之后 Node.js 就在 v6.3 中加入了這個(gè)功能:
并且在成熟之后去掉了對(duì) v8 debug protocol 的支持,也就是廢棄了 node debug 命令,改為了 node inspect。
啟動(dòng) ws 服務(wù)的方式就是 node --inspect 或者 node --inspect-brk。
當(dāng)然,之前作為兩個(gè)協(xié)議的中轉(zhuǎn)的服務(wù) node-inspector 也就退出了歷史舞臺(tái)。
所以今天,我們可以輕易的用 Chrome Devtools 來調(diào)試 Node.js 代碼,就如本文開始展示的那樣。
當(dāng)然,這里只是說 Chrome Devtools 調(diào)試 Node.js,在 VSCode 里調(diào)試 Node.js 的話還有另一段小故事:
調(diào)試的原理我們已經(jīng)知道了,就是 ws 客戶端和服務(wù)器的通信,然后基于調(diào)試協(xié)議來完成不同的功能。Node.js 是這樣,其他語言也是這樣。
VSCode 是一個(gè)通用的編輯器,是要支持多種語言的,也就是它的調(diào)試 UI 要支持多種調(diào)試協(xié)議。
要同一個(gè)調(diào)試工具同時(shí)支持不同的協(xié)議有點(diǎn)不太現(xiàn)實(shí),那怎么辦呢?
可以加一個(gè)中間層,VSCode 的調(diào)試 UI 只要支持這個(gè)中間的調(diào)試協(xié)議就可以了,其余的調(diào)試協(xié)議適配到這個(gè)調(diào)試協(xié)議上來:
這就是 DAP 協(xié)議,debugger adpater protocol。
Node.js 在把調(diào)試工具的協(xié)議換成兼容 Chrome Devtools Protocol 的協(xié)議之后,只要實(shí)現(xiàn)個(gè) DAP 的 adapter 就可以對(duì)接到 VSCode 的調(diào)試工具了。
這樣我們就可以在 VSCode 里調(diào)試 Node.js 了。
Node.js 調(diào)試的故事講完了,我們來總結(jié)下:
總結(jié)
現(xiàn)在 Node.js 的調(diào)試可以用 Chrome Devtools 也可以用 VSCode,都是挺方便的。
但是它不是一開始就這么好用的,我們聊了下它之前的故事:
調(diào)試的原理就是 Node 啟動(dòng) ws 的調(diào)試服務(wù),調(diào)試客戶端(chrome devtools、vscode 等)對(duì)接這個(gè)調(diào)試服務(wù)并實(shí)現(xiàn)交互的 UI,基于傳輸?shù)恼{(diào)試協(xié)議來完成調(diào)試。
最開始 Node.js 的調(diào)試協(xié)議是 v8 debug protocol,只能用命令行調(diào)試。
為了直接用 Chrome Devtools 的 UI 來調(diào)試,就實(shí)現(xiàn)了 node-inspector 的中轉(zhuǎn)服務(wù)來實(shí)現(xiàn) v8 debug protocol 到 chrome devtools protocol 的協(xié)議轉(zhuǎn)換。
這樣還是太麻煩了,所以后來 Node.js 和 v8 團(tuán)隊(duì)合作實(shí)現(xiàn)了 v8-inspector,可以讓 Node.js 提供的調(diào)試協(xié)議是直接兼容 Chrome Devtools Protocol 的。
這樣我們就可以直接用 node --inspect 起 ws 調(diào)試服務(wù),然后用 Chrome Devtools 連接調(diào)試了。
VSCode 為了同一個(gè)調(diào)試 UI 支持不同語言的調(diào)試,設(shè)計(jì)了中間的調(diào)試協(xié)議 Debug Apapter Protocol。Node.js 想在 VSCode 里調(diào)試的話只要實(shí)現(xiàn)對(duì)應(yīng)的 adapter 即可。
今天我們用 Chrome Devtools 或者 VSCode Debugger 都可以輕松調(diào)試 Node.js 代碼,其實(shí)這背后還是有一段挺有意思的故事的。