Node.js调试指南
Posted 小平果118
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Node.js调试指南相关的知识,希望对你有一定的参考价值。
现今 Node.js
愈发受欢迎,应用场景也越来越多,学会高效调试 Node.js
会让日常开发更高效。下面讲下使用inspector
调试nodejs
程序
Node6.3+
的版本提供了两个用于调试的协议:v8 Debugger Protocol
和 v8 Inspector Protocol
可以使用第三方的 Client/IDE
等监测和介入 Node(v8) 运行过程,进行调试。
v8 Inspector Protocol
是新加入的调试协议,通过 websocket
(通常使用 9229 端口)与 Client/IDE
交互,同时基于 Chrome/Chromium
浏览器的 devtools
提供了图形化的调试界面。
1 开启调试
1.1 调试服务器代码
如果你的脚本搭建http
或者net
服务器,你可以直接使用--inspect
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
是指定的端口号
# 控制台会输出如下:
/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
;
1.2 调试脚本代码
如果你的脚本运行完之后直接结束进程,那么你需要使用--inspect-brk
来启动调试器,这样使得脚本可以代码执行之前break,否则,整个代码直接运行到代码结尾,结束进程,根本无法进行调试。
node --inspect-brk=9229 app.js
2 调试工具接入
2.1 VS Code
Vs Code
内置了 Node debugger
,支持 v8 Debugger Protocol
和 v8 Inspector Protocol
两种协议。对于 v8 Inspector Protocol
,只需要在配置里添加一条 Attach
类型配置
在 Debug
控制面板, 点击 settings
图标,打开 .vscode/launch.json
.
点击 “Node.js” 进行初始配置即可.
"version": "0.2.0",
"configurations": [
"type": "node",
"request": "launch",
"name": "Launch Program",
"program": "$workspaceFolder/app.js"
]
2.2 Chrome DevTools
- 方法1:在Chrome浏览器打开
chrome://inspect
点击Configure
按钮,确定host和端口在列表中。
- 方法2:从上述host和端口
/json/list
复制devtoolsFrontendUrl
或–-inspect
提示信息,并复制到Chrome.
2.2.1 Console Panel
chrome
接入要调试的 node
进程后,可以在 Console
中代理 Node
进程中所有的控制台输出,提供了灵活的 Filter 过滤功能,还可以在 Node 进程代码的上下文中直接执行代码。
2.2.2 Sources Panel
Sources
中可以查看所有加载的脚本,还包括第三方库和Node
核心库,选中文件可以进行编辑,Ctrl + C
保存可以直接修改运行中的脚本。
2.2.3 Profile Panel
Profile
用于对运行中的脚本进行性能监测,包括CPU和内存的使用,CPU profile
,可以记录时间线上 javascript 函数执行时占用的 CPU 时间.
profile 记录时间段有两种
- 手动开始/停止:单击
start
开始记录,单击stop
停止记录 - 在代码中插入开始/停止的 API 调用
console.profile('tag')
console.profileEnd('tag')
,可以在Sources
面板中直接编辑保存代码,然后 F5 刷新一下。
profile有三种视图
- chart:俗称火焰图,以时间为横轴显示函数调用栈。下面简单举例分析
火焰图的函数调用栈是倒置的,最上面为栈底,最下面为栈顶。一个栈是一个 tick
,一个 tick
一定是由 Node
底层开始调用的,在 Node 中使用 process.nextTick(fn)
和setTimeout(fn, deloy)
的系统回调会产生新的 tick
,对应产生新的调用栈。
函数的调用顺序是从栈底到栈顶。上图中第一个栈 parserOnHeadersComplete
由底层调用,parserOnHeadersComplete
中调用了 parserOnIncoming
, parserOnIncoming
中调用了emit
…依次类推。
调用栈的宽度是函数执行的时间。一个函数的执行时间包含了其内部调用其他函数的执行时间,所以相对靠近栈底的函数的调用时间一定比靠近栈顶的函数的调用时间长。除去内部调用其他函数的执行时间,就是当前函数的执行时间。
点击函数会跳转到 Sources
面板中函数定义的位置。
将鼠标悬停在函数上可显示其名称和数据:
下面解释摘自 chrome-devtools 文档
-
Name
:函数的名称。 -
Self time
:完成函数当前的调用所需的时间,仅包含函数本身的声明,不包含函数调用的任何函数。 -
Total time
: 完成此函数和其调用的任何函数当前的调用所需的时间。 -
URL
:形式为 file.js:100 的函数定义的位置,其中 file.js 是定义函数的文件名称,100 是定义的行号。 -
Aggregated self time
:记录中函数所有调用的总时间,不包含此函数调用的函数。 -
Aggregated total time
: 函数所有调用的总时间,不包含此函数调用的函数。 -
Not optimized
:如果分析器已检测出函数存在潜在的优化,会在此处列出。 -
heavy(Bottom Up):统计数据,自底向上,底指的是火焰图的底。
- tree(Top Down):统计数据,自顶向下,顶指的是火焰图的顶。
可以看到程序大部分时间是消耗在longCall
这个函数的调用上;
2.2.4 Memory profile
堆分析器可以按页面的 JavaScript 对象和相关 DOM 节点显示内存分配(另请参阅对象保留树)。使用分析器可以拍摄 JS 堆快照
、分析内存图
、比较快照
以及查找内存泄漏
.
3. Node Inspector 代理实现
通过 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
进行动态替换到请求头上。
画了一张流程图:
3.1 Tcp 代理
首先,先实现一个 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
到你的应用。
3.2 获取 websocketId
这一步开始,就是为了解决 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
了。
3.3 Inspector 代理
拿到了 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。
以上是关于Node.js调试指南的主要内容,如果未能解决你的问题,请参考以下文章