本文由體驗技術團隊屈金雄原創。
Node.js 是一個開源的、跨平臺的 JavaScript 運行時環境,它允許開發者在服務器端運行 JavaScript 代碼。Node.js 是基于 Chrome V8引擎構建的,專為高性能、高并發的網絡應用而設計,廣泛應用于構建服務器端應用程序、網絡應用、命令行工具等。
本系列將分為9篇文章為大家介紹 Node.js 技術原理:從調試能力分析到內置模塊新增,從性能分析工具 perf_hooks 的用法到 Chrome DevTools 的性能問題剖析,再到 ABI 穩定的理解、基于 V8 封裝 JavaScript 運行時、模塊加載方式探究、內置模塊外置以及 Node.js addon 的全面解讀等主題,每一篇都干貨滿滿。
本文內容為本系列第1篇,以下為正文內容。
inspector 是什么
直接取官方文檔中,對 inspector 的定義:
The node:inspector module provides an API for interacting with the V8 inspector.
翻譯過來就是,inspector 模塊提供了一組用于和 V8 inspector 交互的 API 。
解讀:
- node inspector 是 Node.js 內置模塊
- node inspector 僅提供與 V8 inspector 交互的能力,其本身并沒有調試能力
- Node.js 調試能力來自 V8 inspector
Node.js 調試原理
調試的目的是通過觀察運行時數據來定位問題。Node.js 的運行時數據由 V8 引擎管理,為了實現調試功能,V8封裝了一套 api 供外部查看運行時數據,這套 api 名字就是 V8 inspector(運行時是一個 websocket 服務)。V8 inspector 由于調試協議不同,不能直接與 Chrome DevTools 交互,于是 Node.js 提供了 inspector 模塊,運行時也會啟動一個 websocket 服務,用于適配。
如圖所示,進入 Node.js 調試模式前,主線程需要創建一個 debugger server( websocket 服務,即時通訊服務,也即 node inspector ),用來實現 debugger client(例如 vscode 調試器或 Chrome DevTools ) 與 V8 inspector 通信,V8 inspector 再獲取 Node.js 服務的數據,最終實現單步調試等功能。
經過封裝與簡化后,launch 模式啟動調試時我們甚至感知不到 debugger server 了,但是它一定是存在的。
深入分析 – inspect 參數
分析過程中,我對相關源碼做了粗讀,除了源碼本身,還參考了這篇文章:https://theanarkh.github.io/understand-nodejs/chapter24-Inspector/#11
本文使用的 Node.js 源碼是 18.20.2
如上圖所示,表示的是 Node.js 調試模式啟動過程,大部分節點都是中文表述+函數名。
當我們用 node --inspect test.js 啟動一個 js 腳本時,程序會啟動 debugger server(一個 websocket 服務)。如上圖所示,相關邏輯都在初始化 inspector 部分(藍色節點),接下來細看一下這部分代碼。
下圖的起始節點 server.Start() 函數就是上圖的末端節點 server.Start()。
圖中每一個節點都對應一個函數。無需理解所有節點,我們重點關注著色的幾個節點。
1.第一個藍色節點
當我們運行 node --inspect test.js 命令,可以看到如下打印,打印的內容 Node.js 開發者一定都很熟悉。
Debugger listening on ws://127.0.0.1:9229/43b86c7c-e538-4d5c-98ba-3f5196d8e986
For help, see: https://nodejs.org/en/docs/inspector// 這一行是Node開發者寫的業務代碼的打印
Server running at http://127.0.0.1:3000/
這個藍色節點已經是啟動代碼執行的最后一步了,第一個橙色節點之后的部分在處理連接請求,也就是說,當代碼走到第一個藍色節點時,已經成功啟動了一個 websocket 服務。
通過前面的代碼還能看出,這個 websocket 服務在新起的子線程上運行,正因如此,調試程序才可以在主線程出現異常而崩潰的情況下,記錄發送異常信息數據。
2.第一個橙色節點
注意這個節點代表一個回調函數,這個函數在服務啟動時并沒有執行。
它的執行是由 debugger client(例如 vscode 調試器或 Chrome DevTools )發起的 http 請求觸發的,這次是client發起的第一次請求。
這次請求,對 vscode 調試器來說,就是它的 attach 模式( launch模式是把啟動和連接操作合并了);對Chrome DevTools 來說,感覺上應該是通過輪詢連接的,這個點暫時就不再深入研究了。
3.第二個橙色節點
client 緊接著會發第二次請求(未確認),請求頭會攜帶 upgrade websocket 信息。這時會觸發第二個橙色節點處的回調函數,當識別到是升級請求時,debugger server 才真正升級為 websocket 服務。
4.第二個藍色節點
升級完成后,控制臺會打印“Debugger attached.”,這也是我們調試時常見的控制臺打印信息。
接下來,debugger server 就可以正常處理業務請求了。
5.特別關注一下紅色節點
這里的代碼就可以看出,debugger client 與 debugger server 建立連接的過程中,debugger server 與 V8 inspector 建立了連接。
其實整個初始化 inspector(啟動 debugger server )的過程,是一套完整 websocket 實現,可以作為一個整體來看待。早期 Socket.io 模塊是內置在 Node.js 中的。
– inspect-brk 參數
–inspect-brk 命令,可以在用戶代碼啟動前中斷,相當于在用戶代碼的第一行打了個斷點。
如下圖所示,我們用 node --inspect-brk test.js 命令啟動服務。可以看到,只有 debugger server 啟動成功的提示,沒有 node 服務啟動成功的提示。這是因為在執行用戶代碼前停住了。
這個命令在我們想要研究或調試 node 代碼啟動,又不知道研究對象啟動入口位置時,比較有用
啟動 debugger server 后,我們用連接上,這時可以看到執行停在了業務代碼的第一行,而這一行我們并沒有設置斷點,如下圖所示。
– inspect-wait 參數
這是 node 20 版本新增的啟動參數,用于等待調試器連接后再執行代碼。這樣就可以從執行一開始就開始調試。
Node.js 三種常見的調試方式
本節的介紹,沒有像其他網絡教程那樣手把手,step by step 地寫清楚操作步驟,是因為有講調試原理。
初學者理解本節的前提是先看懂調試原理。
一、vscode 調試
vscode 調試是 Node.js 開發者最推薦的調試方式,因為可以一鍵啟動調試模式,可以不用像 Chrome DevTools 調試那樣起額外的窗口。
vscode 調試 Node.js 分為 launch 和 attach 兩種模式,這里先介紹一下 launch 模式。
launch 模式調試 Node.js
1.創建一個 Node.js 服務,就是 test.js 文件,內容如下:
const { createServer } = require('node:http');const hostname = '127.0.0.1';
const port = 3000;const server = createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');res.end('Hello World');
});server.listen(port, hostname, async () => {console.log(`Server running at http://${hostname}:${port}/`);
});
2.在 Node.js 服務入口文件所在的目錄,點擊 create a launch.json file 按鈕,然后選擇 Node.js 選項(這是選擇調試器),vscode 會自動創建一個 launch.json 文件。
launch.json 文件內容如下,本場景就直接用自動生成的不需要作任何改動。
configurations 下的 request 字段表示的就是我們前面提到的調試模式,默認使用 launch 模式;name 表示這一套配置的名稱,默認名稱是 Launch Program,下一步會用到該名稱;program 表示項目啟動的入口文件。更多調試配置參考 vscode官網
{// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [{"type": "node","request": "launch","name": "Launch Program","skipFiles": [// "<node_internals>/**"],"program": "${workspaceFolder}\test.js"}]
}
如果用如上文件,屏蔽12行,設置了不跳過內部代碼,還可以調試到Node.js的JS源碼。
- 如下圖所示,打斷點,并選擇 Launch Program,然后點擊綠色三角,啟動調試。
attach 模式調試 Node.js
顧名思義,這個模式不會觸發 Node.js 和 debugger server 的啟動,只會作為 debugger client 附加到已經啟動的 Node.js 服務和debugger server 上。如果能理解前面講的調試原理,這里就很好理解了。
1.首先我們要運行 node --inspect test.js,這是在啟動 debugger server。
同時啟動的還有 node 服務,也就是圖中的 http://127.0.0.1:3000。
2.在 launch.json 文件中添加 attach 模式配置
port 字段表示要連接到的 debugger server 的端口號,也就是我們上一步的 9229。
address 字段表示 debugger server 的地址,也就是上一步的 ws://127.0.0.1;debugger server 在本地的話,可以省略該配置。
{// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [{"type": "node","request": "launch","name": "Launch Program","skipFiles": ["<node_internals>/**"],"program": "${workspaceFolder}\test.js"},{"type": "node","request": "attach","name": "Attach Program","port": 9229,// "address": "localhost",},]
}
3.上一步添加完 Attach Program 配置后,多了一個啟動選項,如下圖所示。選擇 Attach Program,然后點擊綠色三角按鈕,啟動調試,即可連接上第一步啟動的 debugger server。
遠程調試
attach模式本地調試不常用,通常用于遠程調試。目前vscode官方提供了兩種遠程調試方案:
- 使用遠程開發擴展包(官方推薦)
這個方案不僅能遠程調試,還能用于遠程開發,比如我們有時可能需要在本地Windows機器上編輯遠程的Linux服務器上的項目。但是要安裝一系列插件,有些復雜
- attach模式連接遠程 debugger server
在前文的attach模式配置中,加一個address字段,填上遠程debugger server的IP地址即可。
二、Chrome DevTools 調試
當我們理解了 Node.js 調試原理,Chrome DevTools 調試就變得手到擒來。
1.首先我們還是要運行 node --inspect test.js,這是在啟動 debugger server。
同時啟動的還有 node 服務,也就是圖中的 http://127.0.0.1:3000
2.接下來再打開 chrome 控制臺,就能看見 Node.js 的圖標。注意沒有啟動 debugger server 的時候,圖標是不會出現的。點擊它,就可以打開 Chrome DevTools。
3.首次打開 DevTools 時,需要在 Connection tab 頁配置準備連接的 debugger server。
可以看到 DevTools 自帶了兩個地址,如果是 debugger server 是在本地啟動,并且使用的是默認的 9229 端口,就不需要添加連接地址了。
4.DevTools 連接 debugger server
之前有過介紹,vscode 連接 debugger server 需要用 attach 模式啟動調試。
DevTools 這邊在完成配置后,不需要任何操作就能自動連上 debugger server。(大概)是因為 DevTools 會輪詢所配置的地址。
連接成功后,會觸發node服務打印“Debugger attached.”提示語。
5.開始調試
我們切到 Sources tab 頁,發現已經有了要調試的代碼。在需要的位置打斷點,再觸發斷點,即可調試。
例如下圖中,斷點位置的代碼是請求處理代碼,我們只需要訪問一下 http://127.0.0.1:3000 這個地址,即可觸發斷點。
三、命令行調試
命令行調試是沒有客戶端之前的方式,現在一般不用,但是如果需要在不方便使用前面介紹的兩種方式的情況下,例如需要在服務器本地調試,可以用命令行調試。
我們把命令的雙橫杠去掉,也就是運行 node inspect test.js 命令,結果如下圖所示:
我們進入到了Node.js的命令行調試模式,具體的用法參考官方文檔的 Debug部分。
接下來貼一些常用命令用法:
步進#
- cont, c:繼續執行
- next, n:下一步
- step,s: 進入方法內部
- out, o:退出當前方法
- pause:暫停正在運行的代碼(類似開發者工具中的暫停按鈕)
斷點#
- setBreakpoint(), sb():在當前行設置斷點
- setBreakpoint(line), sb(line):在特定行設置斷點
- setBreakpoint(‘fn()’), sb(…):在函數主體的第一個語句上設置斷點
- setBreakpoint(‘script.js’, 1), sb(…):在第一行設置斷點 script.js
- setBreakpoint(‘script.js’, 1, ‘num < 4’), sb(…):在第一行設置條件斷點script.js,僅當num < 4 計算結果為true
- clearBreakpoint(‘script.js’, 1), cb(…):清除script.js 第一行的斷點
信息#
-
backtrace, bt:打印當前調用棧
-
list(5):列出腳本源代碼以及 5 行上下文(前后 5 行)
-
watch(expr):將表達式添加到觀察列表,注意表達式需用引號括起來,如watch(“test2”)
-
unwatch(expr):從觀察列表中刪除表達式
-
unwatch(index):從觀察列表中刪除特定索引處的表達式
-
watchers:列出所有觀察者及其值(每個斷點上自動列出)
-
repl:打開調試器的 repl,在調試腳本的上下文中查看數據或表達式
-
exec expr, p expr:在調試腳本的上下文中執行表達式并打印其值
-
profile:啟動 CPU profiling session
-
profileEnd:停止當前 CPU profiling session
-
profiles:列出所有已完成的 CPU profiling session
-
profiles[n].save(filepath = ‘node.cpuprofile’):將 CPU profiling session以 JSON 格式保存到磁盤
-
takeHeapSnapshot(filepath = ‘node.heapsnapshot’):獲取堆快照并以 JSON 格式保存到磁盤
注意一下,repl命令可以進入斷點所在上下文,方便地查看數據或表達式:
inspector 模塊的 API
Node.js 的 inspector 模塊提供了一組 API,用于在運行時與 V8 引擎進行交互,調試和分析 Node.js 應用程序。
這些 API 使開發者可以通過編程方式啟動調試會話、設置斷點、執行調試命令、收集性能數據等。
這里僅詳細介紹兩個方法:
1.inspector.Session 類的 post 方法可以向 v8 inspector 發送消息(指令),獲取各種信息。v8 inspector 能識別的指令可以在 Chrome DevTools Protocol 中查看
貼一份官網的示例,對該類的能力可見一斑。
import { Session } from'node:inspector/promises';
import fs from'node:fs';
const session = newSession();
session.connect();await session.post('Profiler.enable');
await session.post('Profiler.start');
// Invoke business logic under measurement here...// some time later...
const { profile } = await session.post('Profiler.stop');// Write profile to disk, upload, etc.
fs.writeFileSync('./profile.cpuprofile', JSON.stringify(profile));
2.inspector.open方法可以在節點啟動后,以編程方式啟動debugger server。
vscode有個通過進程id,attach到沒有用–inspect模式啟動的node服務的能力,大概就是通過該接口實現的,暫未確認。
下一節,我們將講解如何在Node.js中新增一個內置模塊,請大家持續關注本系列內容~學習完本系列,你將獲得:
- 提升調試與性能優化能力
- 深入理解模塊化與擴展機制
- 探索底層技術與定制化能力
關于OpenTiny
歡迎加入 OpenTiny 開源社區。添加微信小助手:opentiny-official 一起參與交流前端技術~
OpenTiny 官網:https://opentiny.design/
OpenTiny 代碼倉庫:https://github.com/opentiny/
TinyVue 源碼:https://github.com/opentiny/tiny-vue
TinyEngine 源碼:?https://github.com/opentiny/tiny-engine
歡迎進入代碼倉庫 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI~ 如果你也想要共建,可以進入代碼倉庫,找到 good first issue標簽,一起參與開源貢獻~