现今 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。