copy form
在纷繁复杂的代码世界中,出错是难免的,也许在传统的前端代码中,你习惯于 console 来排查问题,这是不合理的,在现代的社会下,调试代码是你最快找到问题的方法。
这篇文章就是教你如何快速的使用调试找到问题。查找和识别错误的速度越快,你下班的时间就越早:)。
在当前 Node.js v15 版本下,以前非常多的调试方式已经失效了,Node.js 传统的调试协议也进行了许多升级,我们按照最新的方式,来告诉你如何调试。
为什么要使用调试
众所周知,代码是写(调)出来的,而不是猜出来的。
如果不通过调试运行代码,那么意味着需要去猜测代码中发生的事情,YY 一下,如果代码运行到这个地方,这个值可能是什么。使用调试的主要好处就是可以观察程序的运行情况,而不用做假设,可以一次跟随程序执行一行代码。
另一方面,你可以控制代码执行的逻辑,你可以暂定执行,或者逐行运行,甚至修改内存中的值,让它走到另一个分支里。
Node.js 内置的调试
使用 Node.js 内置的调试方式是最简单直接的,但是现阶段都有 IDE,所以大家都不太关心底层的实现,一键开启调试就行了。
而实际上 IDE 的调试都是基于这个内置调试之上的。
在了解内置的 Node.js 调试方式之前,我们先来了解一下另一个概念:断点(breakpoint)。
断点
顾名思义,断点就是能断住代码执行的点,一般情况下,它的表现真的是个点。
比如 vscode 里的断点(红红的点,十分醒目)。
image.png
断点会强制任何 JavaScript 调试器在给定点暂停。这样就可以让代码执行到这个地方停下,观察这行代码以及之后代码里的变量值。
让我们回归传统,在没有 IDE 的情况下(比如文本编辑器,Vim 啥的),都是使用 debugger
语句来让打断点的。
您使用调试器语句。您可以在代码的任何位置添加此语句,比如:
async function initMethod() { debugger; console.log('bbb');}initMethod();
这样,我们就希望调试的时候会在这一行停下来。
调试模式
光有断点还不行,普通情况下,Node.js 会忽略这个 debugger,只有开了调试模式才会暂停到这一行(原因是调试器太强大,有些恶意行为可以通过它注入代码)。
通过给 node 增加 --inspect
参数才会开启调试模式,这个模式下,还会开放一个默认的 9229 端口,允许其他 IDE 接入。
这个模式下,会输出下面的信息:
Debugger listening on ws://127.0.0.1:9229/d598ab05-88e8-433f-b641-bf2766da97f5For help, see: https://nodejs.org/en/docs/inspector
ws://127.0.0.1:9229/d598ab05-88e8-433f-b641-bf2766da97f5
是暴露的调试链接,里面包含了协议,host,端口和一个唯一的 uuid。这是一个标准 v8 调试协议。
我们执行一下这个命令。
咦,为啥什么反应都没有,代码直接执行结束了,脑中一个大大问号?
事实上,仅仅开启调试还是不够的,调试器还没有接收到足够的信息,或者说没有一个展现调试的地方。
node 还提供了另一个会卡住的调试命令。--inspect-brk
会停在代码的第一行,等待下一步的指示,用他就行了。
但是这只是普通的卡住代码,我们需要能支持 v8 调试协议的 UI。
有许多种方法可以作为 UI,而最简单的就是我们电脑上一般都会有的 Chrome 浏览器。
Chrome 自带了一个调试页 chrome://inspect/
,打开后,如果是在本机,会直接列出可调式的端口和文件地址(如果在远程,也可以配置 ip)。
点击这个 inspect
,添加我们的项目后,蓝色的断点条就乖乖的展现到眼前了。
这个时候,我们就可以进行单步调试了(不需要 debugger 了)。
在 Chrome UI 打开的时候,控制台会输出一句话。
表明这个调试协议已经连上了 node 开启的调试端口。
我们总结一下,整个调试分为两个部分,“开启 node 调试端口” + “符合 v8 调试协议的调试器 attach 到调试端口”。
VSCode 调试
VSCode 是我们最常用的 IDE,集成了调试的 UI,所以我们不再需要开启 Chrome 来调试了。
本质和最基本的一样,开启调试端口,连接调试端口。只是 VSCode 本身是个编辑器,可以直接在其之上打断点,集成度更高,这也是为什么我们一般都使用 IDE 的缘故。
VSCode 提供了一个调试 UI,需要用户配置一个 launch.json(等价于启动命令)。
image.png
内容如下,核心是 runtimeExecutable
使用的命令,以及 runtimeArgs
参数,这里不再需要 --inspect
了(IDE 内部会处理)。
{ // 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。 // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [{ "name": "test", "type": "node", "request": "launch", "cwd": "${workspaceRoot}", "runtimeExecutable": "node", "runtimeArgs": [ "test.js" ], "console": "integratedTerminal", "protocol": "auto", "restart": true, "port": 7001, "autoAttachChildProcesses": true }]}
在上面的配置字段中有个 request
字段,有两个值可以选择:launch
和 attach
, 它表示 VS Code 中核心的两种调试模式。
launch 指的是直接由编辑器启动(直接 fork 一个进程),比如我们这个示例,而 attach 表示服务已经启动,我们是 attach 到原来那个进程中,比如上面的 Chrome 调试。_ 然后打上断点,执行就行了。
执行的时候,我们发现命令行会发现一段话。
cd /Users/harry/project/application/my_midway_app ; /usr/bin/env 'NODE_OPTIONS=--require "/Applications/Visual Studio Code.app/Contents/Resources/app/extensions/ms-vscode.js-debug/src/bootloader.bundle.js" --inspect-publish-uid=http' 'VSCODE_INSPECTOR_OPTIONS={"inspectorIpc":"/var/folders/xw/yl56_kmj5nd_r0cql7rcv8640000gn/T/node-cdp.94650-2.sock","deferredMode":false,"waitForDebugger":"","execPath":"/Users/harry/.nvs/default/bin/node","onlyEntrypoint":false,"autoAttachMode":"always","fileCallback":"/var/folders/xw/yl56_kmj5nd_r0cql7rcv8640000gn/T/node-debug-callback-02a1ac2abe751152"}' /Users/harry/.nvs/default/bin/node test.js
第一个 cd 忽略,我们主要看看中间这段。VSCode 启动的时候加载 bootloader.bundle.js
这个文件,然后传了一堆 IPC 启动参数,比如创建了一个 sock 文件,其余的把 launch 里的参数翻译了一下传入。
核心就是这个 bootlaoder
文件,由于 VSCode 是 ts 写的,这个文件的源码在这。
https://github.com/microsoft/vscode-js-debug/blob/ca280351b2/src/targets/node/bootloader.ts
最核心的代码是 inspectOrQueue
方法,代码如下,其中有几个特别关键的地方。
function inspectOrQueue(env: IBootloaderInfo): boolean { // 省略 // 如果没有传 --inspect,则开启调试端口 const openedFromCli = inspector.url() !== undefined; if (!openedFromCli) { // if the debugger isn't explicitly enabled, turn it on based on our inspect mode if (!shouldForceProcessIntoDebugMode(env)) { return false; } inspector.open(0, undefined, false); } const info: IAutoAttachInfo = { ipcAddress: env.inspectorIpc || '', pid: String(process.pid), telemetry, scriptName: process.argv[1], inspectorURL: inspector.url() as string, waitForDebugger: true, ppid: String(env.ppid ?? ''), }; if (mode === Mode.Immediate) { // 同步模式,直接跟着应用启动,监听调试端口 spawnWatchdog(env.execPath || process.execPath, info); } else { // 异步模式,等进程启动,attach 监听端口 const { status, stderr } = spawnSync( env.execPath || process.execPath, [ '-e', `const c=require("net").createConnection(process.env.NODE_INSPECTOR_IPC);setTimeout(()=>{console.error("timeout"),process.exit(1)},10000),c.on("error",e=>{console.error(e),process.exit(1)}),c.on("connect",()=>{c.write(process.env.NODE_INSPECTOR_INFO,"utf-8"),c.write(Buffer.from([0])),c.on("data",e=>{console.error("read byte",e[0]),process.exit(e[0])})});`, ], { env: { NODE_SKIP_PLATFORM_CHECK: process.env.NODE_SKIP_PLATFORM_CHECK, NODE_INSPECTOR_INFO: JSON.stringify(info), NODE_INSPECTOR_IPC: env.inspectorIpc, }, }, ); } // 省略 return true;}
不管是异步还是同步的模式,其原理都是 Node.js 最基础的 “开启端口”,“连接调试端口” 这两个步骤。VSCode 还会考虑到别的场景,比如代码创建子进程时,会将子进程也自动添加调试参数,方便自动 attach 等。
在这里,我们会发现一个新的名词,叫 AutoAttach
。这是 VSCode 在 2018 年 7 月提出的新名词,微软表示用户基本都不太会写 launch.json 文件,经常写错(没错,就是我),所以为了简化写法,特地做的新功能。
这个功能怎么用呢?
简单的来说,只要启动的 node 加上 --inspect
命令,VSCode 就能自动监视到,并且 attach 到进程里开启调试,不再需要复杂的配置。
开启的命令加到了选项里(cmd+shift+p 搜索)。
有几种附加方式。
比较常用的是仅带标志。
这样我们只要在 VSCode 终端里输入任意带有 --inspect
的命令,就会自动被断点到了,很香。
总结一下
调试到这里基本就讲完了,所有的调试的原理都是一样的,藉由 Node.js 原生的打开调试端口的能力,不同的 IDE 才能连接到该端口,进而做出更加强大的能力。
比如 VSCode 不仅仅能做传统的调试,也能增加配置,在执行调试前后增加钩子,执行自己的命令,这都是扩展能力的体现。
相信你看完这篇文章,对 Node.js 应用的调试方式有了一定的理解,写出更好的代码。
https://mp.weixin.qq.com/s/7PNE3nBhpQOTN4stChvWzQ