嘍,你好啊,我是雷工!
斷點調試是程序猿必備的調錯,梳理邏輯的技能;當遇到程序報錯,或者程序邏輯理解不了,都可以通過斷點調試來輔助解決遇到的問題。
斷點調試是程序猿必不可少的技能,本節學習斷點調試,以下為學習筆記。
● 作用:學習時可以幫助更好地理解代碼運行,工作時可以更快找到bug
● 斷點調試步驟:
1.1、選運行程序;
1.2、在瀏覽器打開調試界面(按F12打開開發者工具)
1.3、在瀏覽器控制臺中選中sources一欄;
1.4、單擊對應的html頁面;
1.5、在代碼第一行位置處設置斷點(在需要設置斷點的對應行上點擊鼠標左鍵);
1.6、重新刷新界面,執行程序;
1.7、手動讓程序逐行執行,點擊F10或者點擊下一步按鈕。
1.8、將鼠標放到變量上或者某個條件上就可以看到執行的結果了。
● 斷點:在某句代碼上加的標記就叫斷點,當程序執行到這句有標記的代碼時會暫停下來。
2、循環嵌套:
說明:一個循環中可以嵌套一個或多個循環。
利用斷點調試,可以很好的理解循環嵌套程序。
音小程序開發者工具(https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/developer-instrument/overview)是面向字節系小程序開發者推出的桌面端集成開發環境,支持小程序開發、調試、預覽、上傳等基本功能,旨在幫助開發者更高效地開發小程序,我也是負責本地開發能力的建設。
因為工作原因最近對斷點調試進行一些研究,百度了一下,遺憾的是發現網絡上大部分內容都是在教學如何使用調試工具,并沒有擴展到具體的細節,譬如通信邏輯,基本原理等。因此,為了嘗試去弄懂一些斷點調試的底層邏輯,特意去找了一些英文文檔并實踐。
作為一個前端開發,前端調試的方式一般有如下幾種:
相比于 console,debugger 可以看到代碼實際的執行路線以及每個變量的變化,代碼可以跳著看,也可以針對某個函數步步執行。
但是 console 與 debugger 方式對代碼都有侵入,在開發階段可能要不斷增加和移除來調試,如果不小心忘了,那 mr 又得打回并重新提交了…
相信很多人在提 mr 都有類似經驗…
相對來說,瀏覽器中找到 source 源碼打斷點是一個更好的方式,但是還是需要打開 Devtools ,并在 sources 面板找到文件注入斷點,操作上也是有點小麻煩。
因此第 3 種方式,可能是不錯的方式,在 vscode 中直接在源碼中調試,并能看到具體的變量信息和網頁效果。
實際上,瀏覽器打斷點與在 vscode 打斷點本質原理都類似。下面就聊一聊瀏覽器斷點調試和 vscode 斷點調試的原理。
在了解具體場景之前,首先有一個比較重要的概念,那就是 CDP。
CDP(Chrome DevTools Protocol)是一種通過網絡協議與 Google Chrome 或其他兼容的瀏覽器進行通信的協議。通過 CDP,開發者可以遠程控制瀏覽器,獲取瀏覽器狀態信息,以及執行各種瀏覽器操作,從而實現自動化測試、性能分析、調試等應用場景。
:
CDP 最早于 2011 年在 Chrome 15 版本中引入,作為 Chrome DevTools 的核心組件之一而出現。在此之前,開發者通常需要通過瀏覽器插件或者第三方工具來進行調試和測試,這些工具通常不夠標準化和通用,也難以實現遠程控制。
就跟 Emoji 的歷史差不多了,都是亂的,然后規范化,最后大力發展。
CDP 的出現解決了這些問題,使得開發者可以通過標準化的協議來遠程控制瀏覽器,獲取瀏覽器狀態信息,以及執行各種瀏覽器操作。CDP 的出現和發展推動了 Web 開發和測試的發展,為開發者帶來了更加高效和便捷的開發和測試方式。
CDP 通過 JSON-RPC 協議來進行通信,提供了一套完整的 API,包括 DOM、CSS、網絡、調試、安全等方面的接口。實際上,可以使用各種編程語言來編寫 CDP 客戶端,從而實現與瀏覽器的交互。
上圖為 CDP 的官網(https://chromedevtools.github.io/devtools-protocol),可以看到,CDP 包括很多 Domains,常見的 CDP 信息包括:
這幾個也是平常開發中最常用到的幾個 Domains 了。
chrome 的 Devtools (Front-End Devtools)與 Web Page 之間的調試也是通過 CDP 通信的,如下圖所示:
除了調試,CDP 額外應用場景也很多,比如剛才提到的自動化測試,通過 CDP 模擬用戶行為,操作頁面元素等,或者 CDP 獲取瀏覽器的性能指標生成性能報告,還可以通過 CDP 模擬瀏覽器行為,獲取頁面數據,實現爬蟲等等。
帶著問題出發,可能需要搞懂以下 3 點:
頁面與 Devtools 是如何通信的?
斷點操作邏輯通信過程是什么?
如何實現命中斷點并停止代碼執行的?
在瀏覽器中,網頁的調試能力是由 Devtools 提供的。Devtools 與網頁之間的通信利用的是 Websocket,而通信協議則是 CDP。
除了開發中常用到的元素高亮,日志打印和網絡審查,上面也提到了還可以在 sources 面板中使用 debugger。
如下圖所示,找到一行 js 代碼,在代碼中點擊斷點調試,可以看到 Protocol Monitor 中有一些 CDP 消息,下面就來具體分析一下相關 CDP 信息。
為什么會發送多次,我也不理解,內容基本上是一致的。
點擊斷點以后,主要有以下一些 CDP 消息在頁面與 Devtools 之間通信:
setBreakpointsActive 表示告訴頁面要設置一個調試斷點了;setBreakpointByUrl 則是告訴頁面設置的具體信息;getPossibleBreakpoints 表示設置以后獲取正確的斷點位置,并展示藍色小塊。
有時候可能會發現設置了某一行為斷點,但是斷點的位置并不是指向的位置,而是另外的位置。比如上面截圖,如果在 15 行設置斷點,則最后展示斷點位置為 18 行。
整體流程如下圖:
除了在 sources 面板增加斷點,還可以取消斷點。取消斷點的 CDP 非常簡單, Devtools 會給 Web Page 發送一個 Debugger.removeBreakpoint 來移除斷點。
當點擊完斷點以后,頁面會走到斷點所在的代碼位置,同時 Devtools 會接收到一些 CDP 消息,通知它當前斷點的狀態和上下文信息。
我寫了一個實例,是關于數字的增減邏輯,并在數字增加的時候,走到斷點位置(不需要刷新頁面)。
可以看到,當點擊 + 號以后,頁面就進入斷點調試邏輯,此時 Devtools 會收到 Debugger.paused消息:
此時表示頁面已經暫停了代碼執行,Devtools 可以通過 Debugger.paused事件中的參數,獲取當前斷點的上下文信息,如斷點所在的函數、變量值、堆棧信息等。
具體信息沒有對應看
點擊“Step Over next function call”(按鈕 1),Devtools 會收到 Debugger.resumed r??zu?m d 消息,通知繼續執行代碼。
隨后代碼跳到下一行,此時又會收到 Debugger.paused消息。
點擊“Resume Script Execution” (按鈕 2)按鈕,Devtools 會收到 Debugger.resumed消息,如果還存在斷點,則此時也會收到 Debugger.paused消息。
此外這里還有一個 Overlay.setPausedInDebuggerMessage 消息,為 Devtools 發送給頁面,其信息主要是讓頁面展示代碼停止狀態下應該展示的消息,默認為 {"message":"Paused in debugger"},也就是如下圖展示的內容:
除了上面兩個按鈕,還有幾個調試按鈕,如下圖綠色區域內:
分別是:Step into next function call、Step out of current function、Step、Deactivate breakpoints。
:
Step into next function call:這個按鈕用于進入當前行代碼所在的函數內部,即單步進入函數中執行。
Step out of current function:這個按鈕用于跳出當前函數,即單步跳出當前函數執行。
Step:這個按鈕用于單步執行代碼,即逐行執行代碼。
Deactivate breakpoints:這個按鈕用于禁用所有的斷點,即暫停調試器的所有斷點。
點擊“Step into next function call”,Devtools 會發送 Debugger.stepInto 消息,并收到 Debugger.resumed和 Debugger.paused消息,進入到函數內部。
點擊“Step out of current function”,Devtools 會發送 Debugger.stepOut消息,并收到 Debugger.resumed和 Debugger.paused消息,跳出該函數。
點擊 “Step” 按鈕,Devtools 則發送 Debugger.stepInto,代碼執行到下一行,每次點擊,都會發送 Debugger.stepInto消息。
點擊 “Deactivate (/?di??k.t?.ve?t/) breakpoints”,Devtools 則發送 Debugger.setBreakpointsActive 消息。如果當前斷點狀態為執行狀態,則參數為 active: false,同時設置藍色小塊顏色為透明色。
重新執行代碼,斷點調試能力失效。
再點擊一次,則參數為 active: true,斷點調試能力生效。
了解完相關斷點操作流程以后,再分析一下相關邏輯的源碼。
首先,Devtools 的源碼就是 Front-End Devtools,UI 上的邏輯這里就不多分析。關于頁面的調試通信邏輯在 DebuggerModel 中:https://source.chromium.org/chromium/chromium/src/+/main:out/Debug/gen/third_party/devtools-frontend/src/front_end/core/sdk/DebuggerModel.js;l=280;drc=f09c12c84b39d13189a7039a05253ca3766d4751;bpv=0;bpt=0
async stepInto() {
const skipList = await this.computeAutoStepSkipList("StepInto" /* StepInto /); void this.agent.invoke_stepInto({ breakOnAsyncCall: false, skipList }); } async stepOver() { this.#autoSteppingContext = this.#debuggerPausedDetailsInternal?.callFrames[0]?.functionLocation() ?? null; const skipList = await this.computeAutoStepSkipList("StepOver" / StepOver /); void this.agent.invoke_stepOver({ skipList }); } async stepOut() { const skipList = await this.computeAutoStepSkipList("StepOut" / StepOut */);
if (skipList.length !== 0) {
void this.agent.invoke_stepOver({ skipList });
} else {
void this.agent.invoke_stepOut();
}
}
pause() {
this.#isPausingInternal = true;
this.skipAllPauses(false);
void this.agent.invoke_pause();
}
很清晰的看到,上面提到的各種操作邏輯的函數,譬如 pause、stepXXX等 API。
這里列舉幾個操作按鈕通信較多的 API。
pause() 的主要邏輯為 2 點:
stepInto() 的主要邏輯為:
其他 API 邏輯類似。
再分析一下 chromium /?kro?.mi.?m/ 中的斷點調試代碼邏輯。chromium 中發送 CDP 消息到 Devtools 的邏輯在 devtools_agent_host_impl中,而斷點調試邏輯在devtools_session文件中,通過 agent 的 DispatchProtocolMessage最后調用到 session 的 shoulSendOnIO函數。
具體來說,這個函數接收一個包含 CDP 方法的 span 參數,然后檢查該方法是否屬于一組特定的方法,如果是,則返回 true,表示該 CDP 消息需要轉發。
DevToolsSession 是 Chromium 源碼中的一個類,代表一個 DevTools 會話。DevToolsSession 負責管理與 DevTools 和頁面之間的通信,包括上面提到的調試。
bool ShouldSendOnIO(crdtp::span<uint8_t> method) {
static auto* kEntries = new std::vector<crdtp::span<uint8_t>>{
crdtp::SpanFrom("Debugger.getPossibleBreakpoints"),
crdtp::SpanFrom("Debugger.getScriptSource"),
crdtp::SpanFrom("Debugger.getStackTrace"),
crdtp::SpanFrom("Debugger.pause"),
crdtp::SpanFrom("Debugger.removeBreakpoint"),
crdtp::SpanFrom("Debugger.resume"),
crdtp::SpanFrom("Debugger.setBreakpoint"),
crdtp::SpanFrom("Debugger.setBreakpointByUrl"),
crdtp::SpanFrom("Debugger.setBreakpointsActive"),
crdtp::SpanFrom("Emulation.setScriptExecutionDisabled"),
crdtp::SpanFrom("Page.crash"),
crdtp::SpanFrom("Performance.getMetrics"),
crdtp::SpanFrom("Runtime.terminateExecution"),
};
...
}
可以看到,這里定義了所有發送到 Devtools 的 API。在 chromium 的各種斷點調試方法,最后都會調用 DispatchToAgent方法,并走到 ShouldSendOnIO邏輯。
通過上面的分析,了解到了調試器和頁面之間的 CDP 通信內容和 API 的基本實現。那 chromium 又是如何停止代碼到斷點的呢?為何可以停止代碼執行呢?
在 DevTools 中,停止代碼執行到斷點的核心實現是通過使用 V8 JS 引擎中的斷點機制來實現的。當 chromium 執行到一個斷點時,V8 會暫停 JS 代碼的執行,并將控制權轉交給 Devtools。這時候,Devtools 可以執行上述提到的斷點調試的各種操作。
這塊邏輯的代碼在 chromium auction_v8_devtools_agent 和 auction_v8_devtools_session 中,看起來比較復雜,涉及到 AuctionV8DevToolsSession 和 AuctionV8DevToolsAgent 兩個類,我的理解是 DevtoolsAgent 提供了一些 Devtools debugger 的服務,并找到對應的 DevtoolsSession 進行通信。V8 將 ws 格式信息轉交給了 DevtoolsSession,最后通過 DevtoolsAgent 發送到了 Devtools。
大概邏輯如下:
通過 Devtools Agent,負責接收 Devtools 通信信息,并將斷點信息移交給 V8,然后由 V8 來對代碼進行停止操作。
V8 里面的邏輯我只能看一個大概,整體邏輯如下:
V8Debugger 是一個抽象,V8DebuggerAgentImpl 類實現了這個類,它是 Debug 類和 V8 調試協議之間的中介,負責將調試消息轉換為 V8 調試協議中定義的格式。
關于 V8 斷點 Debugger 更底層的邏輯是與 os、cpu 相關,os 提供了系統調用來實現可執行代碼的中斷。
中斷則是 cpu 執行下一條指令之前,關注一下中斷標記,從而判斷是否需要中斷執行。整體邏輯上對照著 Vue 的渲染原理即可,每次事件循環結束后最后去走一次渲染 DOM。
V8 本身也是將 JS 轉為可執行語言,這也就是為何 JS 可以在瀏覽器中擁有斷點能力了。
這里涉及到一些指令操作,沒有深究。
同時,V8 中斷代碼執行,也會提供一些環境數據到 Devtools,譬如當前變量數值等,這時候 V8 就會將這些調試信息通過 V8 Debug Protocol 協議的格式丟給 Debug,最后丟給 Devtools,從而鼠標懸浮在 sources panel 即可看到對應的數據內容。
Debugger.evaluateOnCallFrame 和 Runtime.getProperties 可以拿到一些環境信息,前者比如一些 number 數字就可以得到。
在 Vscode 中調試代碼,能讓開發者專注于代碼本身,一邊開發運行一邊斷點調試查看變量信息,并減少一些臟代碼的開發。如下圖所示,可以看到,似乎是將瀏覽器的 Debugger 的邏輯照搬到了 Vscode 中。
在介紹完瀏覽器斷點調試的邏輯以后,我們大概了解了頁面與 Devtools 的通信過程和相關 CDP 信息。有了這些基礎,我們再分析分析 Vscode 中是如何實現斷點調試 Web 代碼的。
在 Vscode 中配置調試后,會生成一個 .vscode/launch.json 文件,其主要是配置需要調試的 url 和遠程調試的端口號 port。
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "針對 localhost 啟動 Chrome",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
}
]
}
[?ɑrk??tekt??r]
Vscode 并不只是前端開發者調試 JS 使用,還可以調試其他語言,Python 一些教程就建議使用 Vscode 調試。因此 Vscode 的調試架構高度靈活,可以支持多種編程語言和調試場景,并且可以基于該架構實現各種調試擴展。
如上圖,Vscode 的調試架構中,有 3 個 Core Module:
:別忘了另外一個 Debugger,即為 launch.json 中的 type,指底層的調試目標,例如 Node.js 運行時、Chrome 瀏覽器等等。比如斷點后的信息需要傳遞給 chrome,需要去暫定代碼執行,并斷點逐步執行等。
在了解原理之前,先看一些現象:
通過上面 3 種現象可以看出,Vscode Webpage Devtools 關系如下:
細品一下,這時候就可以知道為何需要 Debug Adapter 了。實際上,就是將 CDP 消息轉為 DAP。
Vscode Chrome Debug 的工作流程如下:
這里的核心就是 Extension,其作用就是調度與控制,比如啟動 Adapter 進程,發送與接收調試信息等等,屬于大 BOSS,而 Adapter 只是下屬。
上面提到,chromium 內部是使用 CDP 協議通信,因此 Extension 想要正確調試 Chrome WebPage,首先就得遵守 Chrome 的玩法。比如,在 Vscode 中點擊 StepInto 按鈕,這時候會將對應操作信息轉化為 CDP 信息,然后再發送給 WebPage。
Extension 啟動 Chrome 的邏輯在 companionBrowserLaunch 中:https://github.com/microsoft/vscode-js-debug/blob/main/src/ui/companionBrowserLaunch.ts#L50
await vscode.commands.executeCommand('js-debug-companion.launchAndAttach', {
proxyUri: tunnel ? 127.0.0.1:${tunnel.localAddress.port} : 127.0.0.1:${args.serverPort},
wslInfo: process.env.WSL_DISTRO_NAME && {
execPath: process.execPath,
distro: process.env.WSL_DISTRO_NAME,
user: process.env.USER,
},
...args,
});
另外,Devtools 與 WebPage 是通過 ws 通信的,這里 JavaScript Extension 內部實現與開發者工具調試器和模擬器的通信相似, Extension 與 WebPage 通信也是拿到了頁面的 debug ws url,在 Extension 內部創建一個 ws client,通過該 client 監聽來自于 WebPage CDP 信息,并轉發到會話的 Adapter,最后再交給 Vscode。
看最新的代碼,JS Debug Extension 也會負責部分調試 UI 相關邏輯。
以 StepInto舉例,在 Vscode 中點擊該按鈕以后,會發送一個 DAP 消息:
{
"command": "stepInTo",
"seq": number,
"type": "request",
"arguments": {
"threadId": number
}
}
然后,Exetension 將該消息轉為 CDP 消息,并發送給 WebPage:
{
"id": 1,
"method": "Debugger.stepInto",
"params": {
"callFrameId": number/string
}
}
WebPage 收到該消息后,返回執行結果到 Extension:
{
"id": 1,
"result": {}
}
Extension 再將該 response 通過 Debug Adapter 轉給 Vscode,Vscode 調整 UI:
{
"body": {
"reason": "OK",
"threadId": number
},
"type": "response"
}
相關 DAP 格式可以在 debug-adapter-protocol 查閱:https://microsoft.github.io/debug-adapter-protocol/overview
如果要在 Vscode 中查看實時的 DAP 和 CDP 消息,可以通過如下操作:
上面給到的例子非常簡單,js 代碼也沒有經過構建生成編譯后的代碼。但是實際場景中開發的項目會引入各種開源庫,然后經過諸如 Webpack 等打包構建工具做編譯打包,才能在瀏覽器中運行。編譯壓縮后的代碼一般不具備可讀性,因此在編譯后代碼進行調試成本比較高。
We all know,SourceMap 存儲著源碼和生產代碼之間的映射關系。譬如我這里啟動了一個 Vite 項目:
當我在源碼的 main.ts 中設置斷點時,可以看到 Request 中的 url 為 host:port/src/main.ts,即實際傳給 WebPage 的斷點文件為編譯后的文件。
JS Debug Extension 亦是如此。
當在 Vscode 的源碼中增加了一個斷點,JS Debug Extension 會根據 sourceMap 將源代碼路徑映射到編譯后的代碼路徑中,并將這個信息發送給瀏覽器。
所以呀,解析是前端行為。
SourceMap 雖然也是靜態資源,但是其加載在 Network 面板并不能看到,而是在 Developer Resources 中。
為了啟動快,我用的 Vite 來生成項目。Vite 利用了瀏覽器原生的 ES modules 功能,根據文件依賴關系,生成依賴樹,然后各模塊文件模塊單獨加載。Vite 文件都有單獨的 SourceMap,不需要配 SourceMap 依賴。
可以看到,這里 Vite 默認是直接內嵌的 SourceMap,無需單獨請求, 可以在代碼文件加載完成后,就直接解析了,紅框里面展示的鏈接就是 Base64 的形式了。
??SourceMap 的解析是交給 Devtools 本身的,Debugger 只負責運行和暫停。因此,如果斷點在 SourceMap 解析完成之前觸發,則沒法告訴 Debugger 正確的地址,可能會出現斷點無效情況。
根據上面的介紹,小程序斷點調試的最簡單辦法就是在代碼中寫上 debugger,然后交給 v8 處理即可。另外還有一種方式就是打開小程序調試器,在 sources panel 中打斷點,如下圖:
打斷點,刷新小程序,即可跳轉到斷點位置。此時可以看到對應的 CDP 消息中的 Request。
可以看到,這里點擊的是 56 行,但實際上 Request 中卻不是,Devtools 通過 sourceMap 進行了處理,定位到了 64 行。根據上面提到的源碼調試邏輯,這里的位置為編譯后的代碼位置,找到編譯產物代碼 app.js 即可看到 real position。
考慮到上面提到的 Vscode 有 web 斷點調試能力,那 IDE Editor 或許也是可以支持斷點調試能力的。
Vscode 可以直接在編輯器運行項目,然后啟動自定義的調試目標(Debugger)。
IDE 為小程序運行時的載體,與 Vscode 啟動 web 項目不一樣,其邏輯為編譯完成后生成一個編譯產物目錄,通過靜態服務,Simulator 直接加載對應編譯產物。因此,IDE 的 Editor 實際上跟 Simulator 沒什么聯系的。
假設借用 Devtools Debug 的邏輯,當在 Editor 打斷點時,捕獲所有的斷點 DAP 消息,當開啟調試時,刷新模擬器,將所有的斷點信息轉為 CDP 信息發送給模擬器,或許就可以簡單實現該能力。
當然,考慮到是在源碼中打斷點,這里的難點應該是在于要實現 sourceMap 解析,而 Debug UI 則可以利用 Vscode JS Extension,或者通過自定義實現一個 Debug UI。
本文從抖音開發者工具支持斷點調試能力需求引入,概述了瀏覽器斷點調試的基本原理,也介紹了 Vscode Web 代碼斷點調試能力,詳細介紹了各模塊中各 CDP 消息通信邏輯。閱讀本文可以掌握前端各種調試方法的基本原理。
抖音開放平臺提供小程序、移動應用、網站應用、直播小玩法等多業務載體,為開發者提供豐富的能力和解決方案。抖音開放平臺基于平臺規則和開發者訴求,提供了兩種開放模式:能力開放和行業開放。
[1]
V8 本地調試: https://zhuanlan.zhihu.com/p/568432229
[2]
Debugging over the V8 Inspector Protocol: https://v8.dev/docs/inspector
[3]
Adapter Debug Protocol: https://microsoft.github.io/debug-adapter-protocol/
[4]
SourceMap: https://zhuanlan.zhihu.com/p/615279891
作者:Rabbitzzc
來源:微信公眾號:字節前端 ByteFE
出處:https://mp.weixin.qq.com/s/DGSSDEmAdj8sE_KfN3wQsg
者:陳亦濤來源:大轉轉FE
這篇文章將介紹如何使用斷點來進行 JavaScript 調試。在讀這篇文章之前,需要問一個問題:為什么要使用斷點來進行調試?
我們首先需要認可使用斷點的是必要的,否則下文介紹的所有斷點調試方法都會是廢話。console.log 是前端開發最常用的調試手段,它簡單直接解決一部分問題。但當遇到十分復雜的問題,console.log 就會變得不趁手。比如:
如果你刷過 leetcode 一定深有體會,算法某個測試用例報錯了,有時很難光靠目測找出有問題的那個方法。
花了10分鐘好不容易復現了,但是只跟蹤到某行代碼,需要第二次添加 log 才能繼續尋找問題。查看log -> 添加log -> 查看log... 這個過程重復幾遍,今天剩下的磚就搬不完了。
有 nodejs 服務端開發經驗的同學相信有過在 postman 和 ide 之間反復橫跳的經歷,如果光靠 log,對于一個巨大的復雜對象,控制臺是不好查看全貌的。如果一個接口還涉及到數據庫增刪、第三方依賴,那么復原上一次請求造成的后果也是一件痛苦的事情。
在這些情況下,斷點調試是非常有價值的,將 debug 的時間復雜度從 O(n) 降到 O(1),讓搬磚更快樂。
這是文章的內容大綱:
最簡單的斷點調試,就是在代碼中加一句 debugger,然后到瀏覽器中刷新頁面,這時候瀏覽器就會在 debugger 語句那停止執行。
為了方便理解,引入一個簡單例子,在一個文件夾中創建 index.html 和 index.js,然后在 index.html 中引入 index.js。index.js 內容如下:
// 國際慣例,hello world。
const greet = () => {
const greeting = "hello debugger";
// 瀏覽器執行到這里將會暫停
debugger
console.log(greeting);
};
greet();
console.log("js evaluation done");
執行命令:
npm i -g serve
serve .
然后訪問 http://localhost:5000并打開開發者工具。
這時候我們的 hello world 斷點就打上了,就像這樣:
圖中分為四個區域,藍色區域用于文件選擇,Page 一欄是指當前頁面中的 JS 文件,Filesystem 會顯示我們系統中的文件。通常我們使用 Page。
粉色是代碼的行號和內容。代碼的行號處可以通過點擊來添加新的斷點,再次點擊后取消。
黃色區域用于控制代碼的執行,只需要掌握前四個按鈕的含義,就可以應付絕大多數場景。按鈕1是讓代碼繼續執行(resume),如果遇到下一個斷點就會再次中斷執行。按鈕2可以讓瀏覽器執行當前行(圖中是第3行),然后在下一行中斷代碼,按鈕3是進入當前函數,查看函數具體內容。假設我們當前停在第7行 greet() ,點擊按鈕3就會進入 greet 方法中(也就是第2行)。如果不想再看 greet 方法了,就點擊按鈕4,跳出這個方法,回到第8行。
綠色區域可以查看變量的內容和當前的調用棧。
debugger 是最簡單粗暴的打斷點方式,但是需要修改我們的代碼。需要注意的是,上線前必須刪除這些語句。也可以通過配置 webpack 來自動去除。不過終究還是有些不方便,所以我們來看下如何通過 vscode 來簡化打斷點的方式。
首先我們使用 Vite 來創建一個 Vue 應用用于演示(React步驟類似)。
# 創建 vut-ts 應用
npm init vite
cd hello-vite
npm install
# 調用 VS Code cli 打開項目,
# 或者手動在 VS Code 打開。
code .
npm run dev
然后在 VS Code 中新建一個文件 .vscode/launch.json,填入這些內容:
{
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "launch",
"name": "Launch Vue project",
// 這里填入項目的訪問地址
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
},
]
}
然后使用 cmd+q 退出你正在運行的 Chrome(這步很重要,不能跳過),按 f5 啟動 VS Code 的調試功能。VS Code 就會幫你啟動一個 Chrome 窗口,并訪問上述配置的中的 url。這時候我們的斷點就生效了,可以一步一步地控制代碼的運行,找出 bug 來源。
這里有一個實用的小技巧,就是在 BREAKPOINTS 中,把 Uncaught Exceptions 勾上,這樣在代碼報錯的地方,就會自動中斷執行。當我們遇到一個報錯時,采用這個方法可以省去定位問題代碼的時間。
另外我們可以發現,在 VS Code 斷點生效時,Chrome Devtools 也會同步這個展示這個斷點。
在 VS Code 中,調試有兩種模式,分別是 launch 和 attach。由于真正執行代碼的是 Chrome 中的 JS 引擎,所以是否中斷代碼的控制權是在 Chrome 手里的。那為什么 VS Code 的斷點可以控制代碼的中斷呢?是因為 VS Code 通過 devtools-protocol 向 Chrome 發起指令,告訴 Chrome 需要在哪一行代碼暫停執行。這個發送指令的過程,被稱作 attach。而 launch 的過程包含 attach ,即先 launch(啟動) 瀏覽器,然后 attach(附加) 斷點信息。所以 attach 模式是 launch 模式的子集。
聽起來好像 launch 模式會更方便,為我們省去了手動啟動瀏覽器的過程。但是這存在一個問題,如果同時開發多個前端工程會怎樣?每個工程啟動一個調試進程,就會打開多個瀏覽器,那么在多個瀏覽器之間切換就會顯得很麻煩。我們可以使用 attach 模式解決這個問題。
首先我們使用命令行啟動 Chrome。使用命令行的原因是,我們需要給 Chrome 的啟動傳參。
# 運行這條命令前需要cmd+q退出已運行的Chrome
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222
# 如果看到這個輸出,說明傳參成功。
DevTools listening on ws://127.0.0.1:9222/devtools/browser/856a3533-ca5c-474f-a0cf-88b7ae94c75b
VS Code 和 Chrome 是通過 websocket 交流,--remote-debugging-port 指定了 websocket 使用的端口。然后我們將 launch.json 文件修改成這樣:
{
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "attach",
"name": "Vue Application",
// 項目訪問的 url
"url": "http://localhost:3000",
// websocket 端口,需要與 --remote-debugging-port 參數保持一致。
"port": 9222,
"webRoot": "${workspaceFolder}"
},
]
}
注意在啟動 VS Code 調試之前,需要在 Chrome 中打開 http://localhost:3000 這個頁面。然后我們在 VS Code 中打上斷點,刷新瀏覽器,代碼就成功停在斷點處了。第二個、第n個工程都可以采用相同的配置,區別是 url 字段要根據項目配置進行修改。
上文講的是如何調試頁面,接下來我們聊如何調試 nodejs 應用。首先來一個最容易上手的例子,創建一個 hello world:
// debug.js 文件
const greeting = 'hello nodejs debugger'
debugger
console.log(greeting)
然后運行這個文件
node --inspect-brk debug.js
Debugger listening on ws://127.0.0.1:9229/b9a6d6bf-baaa-4ad5-8cc6-01eb69e99f0a
For help, see: https://nodejs.org/en/docs/inspector
--inspect-brk 表示運行這個 js 文件的同時,在文件的第一行打上斷點。然后打開 Chrome,進入 Devtools。點擊紅框處的按鈕,就會打開一個 nodejs 專用的調試窗口,并且代碼在第一行中斷了。
nodejs 調試窗口:
這個方式的實質是,Chrome Devtool 根據 v8引擎的調試協議 向 nodejs 進程發送指令,控制代碼的運行。可以發現,在網頁的調試中,Chrome 是接受指令的一方,而在 nodejs 調試中,Chrome 轉身變為發送指令的一方。所謂從悲慘的乙方華麗轉身成甲方。
node 默認的 websocket 端口是 9229,如果有需要的話(比如端口被占用了),我們可以通過一些方式改變這個端口。
node --inspect=9228 debug.js
Debugger listening on ws://127.0.0.1:9228/30f21d45-9806-47b8-8a0b-5fb97cf8bb87
For help, see: https://nodejs.org/en/docs/inspector
在我們打開 Devtool 時,Chrome 默認檢查 9229 端口,但當我們改變了端口號后,就需要手動去指定 Chrome 檢查的地址了。點擊下圖中的 Configure 按鈕,輸入 127.0.0.1:9228,然后點擊 Done。這時候 Remote Target 中就會出現 剛才啟動的 node 進程,點擊 inspect 就可以進入調試了。
到此為止,我們已經達成調試 node 的目的,但還有些繁瑣,不夠自動化。我們可以使用 VS Code,來一鍵啟動調試。
用 VS Code 打開剛才的工程,然后在 launch.json 中輸入這些:
{
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
// ${file} 的意思是,當我們啟動調試的時候,調試的程序就是當前 focus 的文件。
"program": "${file}"
}
]
}
這時候切換到 index.js 文件,按 f5 啟動調試程序,當運行到第二行 debugger 語句的時候,就會自動暫停執行。也可以點擊代碼行數的左側來打斷點。
另外,這個配置是支持 TypeScript 的,我們只需要 index.js 重命名為 index.ts,然后正常啟動調試就行。
在某些情況下,我們不希望打上的每個斷點都發揮作用,而是在執行到斷點那行,且滿足某個條件再中斷代碼執行。這就是條件斷點。
for (let i = 0; i < 10; i++) {
console.log("i", i);
}
比如上面的代碼,假設我們在第二行 console.log 打了斷點,那么這個斷點總計會中斷十次。這往往是我們不希望看到的,可能我們需要的僅僅是其中某一次循環而非所有。這時候可以右鍵點擊并選擇 Add Conditional Breakpoint。
這時會有一個輸入框出現,我們在其中輸入 i === 5。
這時候啟動調試,就會跳過 i 為 0 - 4,直接在在 i 為 5 的時候中斷代碼執行。恢復代碼執行后,會略過 i 為 6 - 9 的情況。
Conditional Breakpoint 在調試帶有大量循環和 if else 判斷時極為有用,特別是當某處的邏輯整體上是符合預期的,僅有個別特殊情況的輸出錯誤,使用條件斷點就可以略過這些正常的情況,只在個別特殊情況出現的時候,再中斷執行,供我們查看各個變量是否計算正常。
調試是日常工作中非常重要的能力,因為除了開發新功能外,日常有很大一部分都在調整舊的代碼,處理特別條件下的邏輯錯誤。熟練掌握調試可以很好地提升搬磚幸福感,一個復雜的 bug 卡幾小時,很容易讓人心里崩潰。但也不是說斷點調試是任何情況下都適用的銀彈,簡單的邏輯還是可以愉快地 console.log 的。
文章介紹了使用 Chrome Devtools 和 VS Code 斷點調試的方法,整體上還是更推薦使用 VS Code。launch.json 只需要一次配置,后續都可以 f5 一鍵啟動調試。另外,文中提到的各種 launch.json 文件的配置,都可以使用 VS Code 自帶的工具一鍵生成。只要打開 launch.json,編輯器的右下角就會出現 Add Configuration 按鈕,點擊就可以選擇自己需要添加的調試配置。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。