写 Node.js 代码,从学会调试开始

359次阅读  |  发布于3年以前

在纷繁复杂的代码世界中,出错是难免的,也许在传统的前端代码中,你习惯于 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-bf2766da97f5
For 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 字段,有两个值可以选择:launchattach , 它表示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 应用的调试方式有了一定的理解,写出更好的代码。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8