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