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