現今 Node.js
愈發受歡迎,應用場景也愈來愈多,學會高效調試 Node.js
會讓平常開發更高效。下面講下使用inspector
調試nodejs
程序html
Node6.3+
的版本提供了兩個用於調試的協議:v8 Debugger Protocol
和 v8 Inspector Protocol
可使用第三方的 Client/IDE
等監測和介入 Node(v8) 運行過程,進行調試。node
v8 Inspector Protocol
是新加入的調試協議,經過 websocket
(一般使用 9229 端口)與 Client/IDE
交互,同時基於 Chrome/Chromium
瀏覽器的 devtools
提供了圖形化的調試界面。git
若是你的腳本搭建http
或者net
服務器,你能夠直接使用--inspect
github
const Koa = require('koa')
const app = new Koa()
app.use(async ctx => {
let a = 0
const longCall = () => {
while (a < 10e8) {
a++
}
}
longCall()
ctx.body = `Hello ${a}`
})
app.listen(3000, () => {
console.log('程序監聽了3000端口')
})
複製代碼
使用 node --inspect=9229 app.js
啓動你的腳本,9229
是指定的端口號web
# 控制檯會輸出以下:
/usr/local/bin/node --inspect=9229 src/inspector/demo.js
Debugger listening on ws://127.0.0.1:9229/c4f1e345-e811-47a2-b44a-65f68c0c2cc3
Debugger attached.
# 能夠在瀏覽器裏打開:http://127.0.0.1:9229/json 看到一些信息, c4f1e345-e811-47a2-b44a-65f68c0c2cc3 爲uuid,不一樣調試面板的uuid來區分;
複製代碼
--inspect
對於通常的程序都是一閃而過,斷點信號還沒發送出去,就執行完畢了。 斷點根本不起做用,能夠--inspect-brk
;chrome
若是你的腳本運行完以後直接結束進程,那麼你須要使用--inspect-brk
來啓動調試器,這樣使得腳本能夠代碼執行以前break,不然,整個代碼直接運行到代碼結尾,結束進程,根本沒法進行調試。json
node --inspect-brk=9229 app.js
瀏覽器
Vs Code
內置了 Node debugger
,支持 v8 Debugger Protocol
和 v8 Inspector Protocol
兩種協議。對於 v8 Inspector Protocol
,只須要在配置裏添加一條 Attach
類型配置服務器
在 Debug
控制面板, 點擊 settings
圖標,打開 .vscode/launch.json
. 點擊 「Node.js」 進行初始配置便可.websocket
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"program": "${workspaceFolder}/app.js"
}
]
}
複製代碼
chrome://inspect
點擊Configure
按鈕,肯定host和端口在列表中。/json/list
複製 devtoolsFrontendUrl
或 –-inspect
提示信息,並複製到Chrome.chrome
接入要調試的 node
進程後,能夠在 Console
中代理 Node
進程中全部的控制檯輸出,提供了靈活的 Filter 過濾功能,還能夠在 Node 進程代碼的上下文中直接執行代碼。
Sources
中能夠查看全部加載的腳本,還包括第三方庫和Node
核心庫,選中文件能夠進行編輯,Ctrl + C
保存能夠直接修改運行中的腳本。
Profile
用於對運行中的腳本進行性能監測,包括CPU和內存的使用,CPU profile
,能夠記錄時間線上 Javascript 函數執行時佔用的 CPU 時間.
profile 記錄時間段有兩種
start
開始記錄,單擊 stop
中止記錄console.profile('tag')
console.profileEnd('tag')
,能夠在 Sources
面板中直接編輯保存代碼,而後 F5 刷新一下。profile有三種視圖
tick
,一個
tick
必定是由
Node
底層開始調用的,在 Node 中使用
process.nextTick(fn)
和
setTimeout(fn, deloy)
的系統回調會產生新的
tick
,對應產生新的調用棧。
函數的調用順序是從棧底到棧頂。上圖中第一個棧 parserOnHeadersComplete
由底層調用,parserOnHeadersComplete
中調用了 parserOnIncoming
, parserOnIncoming
中調用了emit
...依次類推。
調用棧的寬度是函數執行的時間。一個函數的執行時間包含了其內部調用其餘函數的執行時間,因此相對靠近棧底的函數的調用時間必定比靠近棧頂的函數的調用時間長。除去內部調用其餘函數的執行時間,就是當前函數的執行時間。
點擊函數會跳轉到 Sources
面板中函數定義的位置。
Name
:函數的名稱。
Self time
:完成函數當前的調用所需的時間,僅包含函數自己的聲明,不包含函數調用的任何函數。
Total time
: 完成此函數和其調用的任何函數當前的調用所需的時間。
URL
:形式爲 file.js:100 的函數定義的位置,其中 file.js 是定義函數的文件名稱,100 是定義的行號。
Aggregated self time
:記錄中函數全部調用的總時間,不包含此函數調用的函數。
Aggregated total time
: 函數全部調用的總時間,不包含此函數調用的函數。
Not optimized
:若是分析器已檢測出函數存在潛在的優化,會在此處列出。
heavy(Bottom Up):統計數據,自底向上,底指的是火焰圖的底。
能夠看到程序大部分時間是消耗在longCall
這個函數的調用上;
堆分析器能夠按頁面的 JavaScript 對象和相關 DOM 節點顯示內存分配(另請參閱對象保留樹)。使用分析器能夠拍攝 JS 堆快照
、分析內存圖
、比較快照
以及查找內存泄漏
.
經過 node inspector
來進行斷點調試是一個很經常使用的 debug 方式。可是之前的調試中有幾個問題會致使咱們的調試效率下降。
vscode
中調試,在 inspector
端口變動或者 websocket id
變動後要重連。devtools
中調試,在inspector
端口變動或者 websocket id
變動後要重連。那 node inspector是如何解決上述兩個問題呢?
對於第一個問題,在 vscode
上,它是會本身去調用 /json
接口獲取最新的 websocket id
,而後使用新的 websocket id
鏈接到 node inspector
服務上。所以解決方法就是實現一個 tcp
代理功能作數據轉發便可。
對於第二個問題,因爲 devtools
是不會自動去獲取新的 websocket id
的,因此咱們須要作動態替換,因此解決方案就是代理服務去 /json
獲取 websocket id
,而後在 websocket
握手的時候將 websocket id
進行動態替換到請求頭上。
畫了一張流程圖:
首先,先實現一個 tcp
代理的功能,其實很簡單,就是經過 node
的 net
模塊建立一個代理端口的 Tcp Server
,而後當有鏈接過來的時候,再建立一個鏈接到目標端口便可,而後就能夠進行數據的轉發了。
簡易的實現以下:
const net = require('net');
const proxyPort = 9229;
const forwardPort = 5858;
net.createServer(client => {
const server = net.connect({
host: '127.0.0.1',
port: forwardPort,
}, () => {
client.pipe(server).pipe(client);
});
// 若是真要應用到業務中,還得監聽一下錯誤/關閉事件,在鏈接關閉時即時銷燬建立的 socket。
}).listen(proxyPort);
複製代碼
上面實現了比較簡單的一個代理服務,經過 pipe
方法將兩個服務的數據連通起來。client
有數據的時候會被轉發到 server
中,server
有數據的時候也會轉發到 client
中。
當完成這個 Tcp
代理功能以後,就已經能夠實現 vscode
的調試需求了,在 vscode
中項目下 launch.json
中指定端口爲代理端口,在 configurations
中添加配置
{
"type": "node",
"request": "attach",
"name": "Attach",
"protocol": "inspector",
"restart": true,
"port": 9229
}
複製代碼
那麼當應用重啓,或者更換 inspect
的端口,vscode
都能自動從新經過代理端口 attach
到你的應用。
這一步開始,就是爲了解決 devtools
連接不變的狀況下可以從新 attach
的問題了,在啓動 node inspector server
的時候,inspector
服務還提供了一個 /json
的 http
接口用來獲取 websocket id
。
這個就至關簡單了,直接發個 http
請求到目標端口的 /json
,就能夠獲取到數據了:
[ { description: 'node.js instance',
devtoolsFrontendUrl: '...',
faviconUrl: 'https://nodejs.org/static/favicon.ico',
id: 'e7ef6313-1ce0-4b07-b690-d3cf5274d8b0',
title: '/Users/wanghx/Workspace/larva-team/vscode-log/index.js',
type: 'node',
url: 'file:///Users/wanghx/Workspace/larva-team/vscode-log/index.js',
webSocketDebuggerUrl: 'ws://127.0.0.1:5858/e7ef6313-1ce0-4b07-b690-d3cf5274d8b0' } ]
複製代碼
上面數據中的 id 字段,就是咱們須要的 websocket id
了。
拿到了 websocket id
後,就能夠在 tcp
代理中作 websocket id
的動態替換了,首先咱們須要固定連接,所以先定一個代理連接,好比個人代理服務端口是 9229
,那麼 chrome devtools 的代理連接就是:
chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:9229/__ws_proxy__
上面除了最後面的 ws=127.0.0.1:9229/__ws_proxy__
其餘都是固定的,而最後這個也一眼就能夠看出來是 websocket
的連接。其中 __ws_proxy__
則是用來佔位的,用於在 chrome devtools
向這個代理連接發起 websocket
握手請求的時候,將 __ws_proxy__
替換成 websocket id
而後轉發到 node
的 inspector
服務上。
對上面的 tcp
代理中的 pipe
邏輯的代碼作一些小修改便可。
const through = require('through2')
client
.pipe(through.obj((chunk, enc, done) => {
if (chunk[0] === 0x47 && chunk[1] === 0x45 && chunk[2] === 0x54) {
const content = chunk.toString();
if (content.includes('__ws_proxy__')) {
return done(null, Buffer.from(content.replace('__ws_proxy__', websocketId)));
}
}
done(null, chunk);
}))
.pipe(server)
.pipe(client)
複製代碼
經過 through2
建立一個 transform
流來對傳輸的數據進行一下更改。
簡單判斷一下 chunk
的頭三個字節是否爲GET
,若是是 GET
說明這多是個 http
請求,也就多是 websocket
的協議升級請求。把請求頭打印出來就是這個樣子的:
GET /__ws_proxy__ HTTP/1.1
Host: 127.0.0.1:9229
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: chrome-devtools://devtools
Sec-WebSocket-Version: 13
複製代碼
而後將其中的路徑/__ws_proxy
替換成對應的 websocketId
,而後轉發到 node
的 inspector server
上,便可完成websocket
的握手,接下來的 websocket
通訊就不須要對數據作處理,直接轉發便可。
接下來就算各類重啓應用,或者更換 inspector
的端口,都不須要更換 debug
連接,只須要再 inspector server
重啓的時候,在下圖的彈窗中
點擊一下 Reconnect DevTools 便可恢復 debug。