寫 Node-js 代碼,從學會調試開始

在紛繁複雜的代碼世界中,出錯是難免的,也許在傳統的前端代碼中,你習慣於 console 來排查問題,這是不合理的,在現代的社會下,調試代碼是你最快找到問題的方法。

這篇文章就是教你如何快速的使用調試找到問題。查找和識別錯誤的速度越快,你下班的時間就越早:)。

在當前 Node.js v15 版本下,以前非常多的調試方式已經失效了,Node.js 傳統的調試協議也進行了許多升級,我們按照最新的方式,來告訴你如何調試。

爲什麼要使用調試

衆所周知,代碼是寫(調)出來的,而不是猜出來的。

如果不通過調試運行代碼,那麼意味着需要去猜測代碼中發生的事情,YY 一下,如果代碼運行到這個地方,這個值可能是什麼。使用調試的主要好處就是可以觀察程序的運行情況,而不用做假設,可以一次跟隨程序執行一行代碼。

另一方面,你可以控制代碼執行的邏輯,你可以暫定執行,或者逐行運行,甚至修改內存中的值,讓它走到另一個分支裏。

Node.js 內置的調試

使用 Node.js 內置的調試方式是最簡單直接的,但是現階段都有 IDE,所以大家都不太關心底層的實現,一鍵開啓調試就行了。

而實際上 IDE 的調試都是基於這個內置調試之上的。

在瞭解內置的 Node.js 調試方式之前,我們先來了解一下另一個概念:斷點(breakpoint)。

斷點

顧名思義,斷點就是能斷住代碼執行的點,一般情況下,它的表現真的是個點。

比如 vscode 裏的斷點(紅紅的點,十分醒目)。

image.png

斷點會強制任何 JavaScript 調試器在給定點暫停。這樣就可以讓代碼執行到這個地方停下,觀察這行代碼以及之後代碼裏的變量值。

讓我們迴歸傳統,在沒有 IDE 的情況下(比如文本編輯器,Vim 啥的),都是使用 debugger 語句來讓打斷點的。

您使用調試器語句。您可以在代碼的任何位置添加此語句,比如:

asyncfunction 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 字段,有兩個值可以選擇: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)) {
      returnfalse;
    }

    inspector.open(0, undefined, false); 
  }

  const info: IAutoAttachInfo = {
    ipcAddress: env.inspectorIpc || '',
    pid: String(process.pid),
    telemetry,
    scriptName: process.argv[1],
    inspectorURL: inspector.url() asstring,
    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,
        },
      },
    );
  }

// 省略

  returntrue;
}

不管是異步還是同步的模式,其原理都是 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 應用的調試方式有了一定的理解,寫出更好的代碼。

轉自:公衆號( Node 地下鐵 ) - 張挺

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/0LLtnL-4R2B1loXhv7Qrpw