做为前端开发,想必大家都写过 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 代码,其实这背后还是有一段挺有意思的故事的。