聊聊網(wǎng)頁斷點(diǎn)調(diào)試及其擴(kuò)展
抖音小程序開發(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ā)小程序,我也是負(fù)責(zé)本地開發(fā)能力的建設(shè)。
因?yàn)楣ぷ髟蜃罱鼘?duì)斷點(diǎn)調(diào)試進(jìn)行一些研究,百度了一下,遺憾的是發(fā)現(xiàn)網(wǎng)絡(luò)上大部分內(nèi)容都是在教學(xué)如何使用調(diào)試工具,并沒有擴(kuò)展到具體的細(xì)節(jié),譬如通信邏輯,基本原理等。因此,為了嘗試去弄懂一些斷點(diǎn)調(diào)試的底層邏輯,特意去找了一些英文文檔并實(shí)踐。
前言
作為一個(gè)前端開發(fā),前端調(diào)試的方式一般有如下幾種:
-
代碼中直接打印,比如很多時(shí)候直接在代碼中使用
console來打印一些變量,或者在 vscode 中使用Turbo Console Log等插件生成特色的日志內(nèi)容。
-
debugger,在代碼中輸入debugger關(guān)鍵字,然后在瀏覽器中進(jìn)行斷點(diǎn)調(diào)試,或者在瀏覽器中找到源碼,然后進(jìn)行斷點(diǎn)調(diào)試。
-
vscode 自帶的 web 調(diào)試能力。

相比于 console,debugger 可以看到代碼實(shí)際的執(zhí)行路線以及每個(gè)變量的變化,代碼可以跳著看,也可以針對(duì)某個(gè)函數(shù)步步執(zhí)行。
但是 console 與 debugger 方式對(duì)代碼都有侵入,在開發(fā)階段可能要不斷增加和移除來調(diào)試,如果不小心忘了,那 mr 又得打回并重新提交了…
?? 相信很多人在提 mr 都有類似經(jīng)驗(yàn)…
相對(duì)來說,瀏覽器中找到 source 源碼打斷點(diǎn)是一個(gè)更好的方式,但是還是需要打開 Devtools ,并在 sources 面板找到文件注入斷點(diǎn),操作上也是有點(diǎn)小麻煩。
因此第 3 種方式,可能是不錯(cuò)的方式,在 vscode 中直接在源碼中調(diào)試,并能看到具體的變量信息和網(wǎng)頁效果。
實(shí)際上,瀏覽器打斷點(diǎn)與在 vscode 打斷點(diǎn)本質(zhì)原理都類似。下面就聊一聊瀏覽器斷點(diǎn)調(diào)試和 vscode 斷點(diǎn)調(diào)試的原理。
基本知識(shí)
Chrome Devtools Protocol
在了解具體場(chǎng)景之前,首先有一個(gè)比較重要的概念,那就是 CDP。
基本概念
CDP(Chrome DevTools Protocol)是一種通過網(wǎng)絡(luò)協(xié)議與 Google Chrome 或其他兼容的瀏覽器進(jìn)行通信的協(xié)議。通過 CDP,開發(fā)者可以遠(yuǎn)程控制瀏覽器,獲取瀏覽器狀態(tài)信息,以及執(zhí)行各種瀏覽器操作,從而實(shí)現(xiàn)自動(dòng)化測(cè)試、性能分析、調(diào)試等應(yīng)用場(chǎng)景。
????:
CDP 最早于 2011 年在 Chrome 15 版本中引入,作為 Chrome DevTools 的核心組件之一而出現(xiàn)。在此之前,開發(fā)者通常需要通過瀏覽器插件或者第三方工具來進(jìn)行調(diào)試和測(cè)試,這些工具通常不夠標(biāo)準(zhǔn)化和通用,也難以實(shí)現(xiàn)遠(yuǎn)程控制。
就跟 Emoji 的歷史差不多了,都是亂的,然后規(guī)范化,最后大力發(fā)展。
CDP 的出現(xiàn)解決了這些問題,使得開發(fā)者可以通過標(biāo)準(zhǔn)化的協(xié)議來遠(yuǎn)程控制瀏覽器,獲取瀏覽器狀態(tài)信息,以及執(zhí)行各種瀏覽器操作。CDP 的出現(xiàn)和發(fā)展推動(dòng)了 Web 開發(fā)和測(cè)試的發(fā)展,為開發(fā)者帶來了更加高效和便捷的開發(fā)和測(cè)試方式。
CDP 通過 JSON-RPC 協(xié)議來進(jìn)行通信,提供了一套完整的 API,包括 DOM、CSS、網(wǎng)絡(luò)、調(diào)試、安全等方面的接口。實(shí)際上,可以使用各種編程語言來編寫 CDP 客戶端,從而實(shí)現(xiàn)與瀏覽器的交互。
上圖為 CDP 的官網(wǎng)(https://chromedevtools.github.io/devtools-protocol),可以看到,CDP 包括很多 Domains,常見的 CDP 信息包括:
-
DOM:提供了對(duì)文檔對(duì)象模型的訪問和操作接口,如節(jié)點(diǎn)遍歷、樣式計(jì)算、事件處理等。 -
CSS:提供了對(duì)樣式表的訪問和操作接口,如樣式計(jì)算、應(yīng)用、修改等。 -
Network:提供了對(duì)網(wǎng)絡(luò)請(qǐng)求和響應(yīng)的訪問和操作接口,如請(qǐng)求攔截、修改、模擬等。 -
Console:提供了對(duì)瀏覽器控制臺(tái)的訪問和操作接口,如日志記錄、錯(cuò)誤捕獲、命令執(zhí)行等。 -
Debugger:提供了對(duì)瀏覽器調(diào)試器的訪問和操作接口,如斷點(diǎn)設(shè)置、單步執(zhí)行、變量查看等。 -
Performance:提供了對(duì)瀏覽器性能分析的訪問和操作接口,如性能指標(biāo)獲取、性能分析報(bào)告生成等。
這幾個(gè)也是平常開發(fā)中最常用到的幾個(gè) Domains 了。
常見 CDP
-
Page:Page.navigate:頁面跳轉(zhuǎn)
-
Network:Network.enable:開啟網(wǎng)絡(luò),可以用來模擬網(wǎng)絡(luò)開閉能力
-
DOM:DOM.getDocument:獲取頁面樹,比如調(diào)試器 Elements 面板的展示
-
CSS:CSS.getComputedStyleForNode:返回給定 nodeId 的所有樣式,比如點(diǎn)擊 dom 節(jié)點(diǎn),展示 css
-
Runtime:Runtime.evaluate:在當(dāng)前頁面中執(zhí)行 JavaScript 代碼
-
Debugger:下面會(huì)提到很多,譬如 Debugger.pause/Debugger.setXXX
應(yīng)用場(chǎng)景
chrome 的 Devtools (Front-End Devtools)與 Web Page 之間的調(diào)試也是通過 CDP 通信的,如下圖所示:
除了調(diào)試,CDP 額外應(yīng)用場(chǎng)景也很多,比如剛才提到的自動(dòng)化測(cè)試,通過 CDP 模擬用戶行為,操作頁面元素等,或者 CDP 獲取瀏覽器的性能指標(biāo)生成性能報(bào)告,還可以通過 CDP 模擬瀏覽器行為,獲取頁面數(shù)據(jù),實(shí)現(xiàn)爬蟲等等。
瀏覽器斷點(diǎn)調(diào)試原理
帶著問題出發(fā),可能需要搞懂以下 3 點(diǎn):
頁面與 Devtools 是如何通信的? 斷點(diǎn)操作邏輯通信過程是什么? 如何實(shí)現(xiàn)命中斷點(diǎn)并停止代碼執(zhí)行的?
操作流程
增加斷點(diǎn)
在瀏覽器中,網(wǎng)頁的調(diào)試能力是由 Devtools 提供的。Devtools 與網(wǎng)頁之間的通信利用的是 Websocket,而通信協(xié)議則是 CDP。
除了開發(fā)中常用到的元素高亮,日志打印和網(wǎng)絡(luò)審查,上面也提到了還可以在 sources 面板中使用 debugger。
如下圖所示,找到一行 js 代碼,在代碼中點(diǎn)擊斷點(diǎn)調(diào)試,可以看到 Protocol Monitor 中有一些 CDP 消息,下面就來具體分析一下相關(guān) CDP 信息。
為什么會(huì)發(fā)送多次,我也不理解,內(nèi)容基本上是一致的。
點(diǎn)擊斷點(diǎn)以后,主要有以下一些 CDP 消息在頁面與 Devtools 之間通信:
-
Debugger.setBreakpointsActive:Activates / deactivates all breakpoints on the page. -
Debugger.setBreakpointByUrl:Sets JavaScript breakpoint at given location specified either by URL or URL regex. -
Debugger.getPossibleBreakpoints:Returns possible locations for breakpoint.
setBreakpointsActive 表示告訴頁面要設(shè)置一個(gè)調(diào)試斷點(diǎn)了;setBreakpointByUrl 則是告訴頁面設(shè)置的具體信息;getPossibleBreakpoints 表示設(shè)置以后獲取正確的斷點(diǎn)位置,并展示藍(lán)色小塊。
有時(shí)候可能會(huì)發(fā)現(xiàn)設(shè)置了某一行為斷點(diǎn),但是斷點(diǎn)的位置并不是指向的位置,而是另外的位置。比如上面截圖,如果在 15 行設(shè)置斷點(diǎn),則最后展示斷點(diǎn)位置為 18 行。
整體流程如下圖:
移除斷點(diǎn)
除了在 sources 面板增加斷點(diǎn),還可以取消斷點(diǎn)。取消斷點(diǎn)的 CDP 非常簡(jiǎn)單, Devtools 會(huì)給 Web Page 發(fā)送一個(gè) Debugger.removeBreakpoint 來移除斷點(diǎn)。
實(shí)時(shí)斷點(diǎn)調(diào)試
當(dāng)點(diǎn)擊完斷點(diǎn)以后,頁面會(huì)走到斷點(diǎn)所在的代碼位置,同時(shí) Devtools 會(huì)接收到一些 CDP 消息,通知它當(dāng)前斷點(diǎn)的狀態(tài)和上下文信息。
我寫了一個(gè)實(shí)例,是關(guān)于數(shù)字的增減邏輯,并在數(shù)字增加的時(shí)候,走到斷點(diǎn)位置(不需要刷新頁面)。
可以看到,當(dāng)點(diǎn)擊 + 號(hào)以后,頁面就進(jìn)入斷點(diǎn)調(diào)試邏輯,此時(shí) Devtools 會(huì)收到 Debugger.paused消息:
-
Debugger.paused:Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria.
此時(shí)表示頁面已經(jīng)暫停了代碼執(zhí)行,Devtools 可以通過 Debugger.paused事件中的參數(shù),獲取當(dāng)前斷點(diǎn)的上下文信息,如斷點(diǎn)所在的函數(shù)、變量值、堆棧信息等。
具體信息沒有對(duì)應(yīng)看
點(diǎn)擊“Step Over next function call”(按鈕 1),Devtools 會(huì)收到 Debugger.resumed r??zu?m d 消息,通知繼續(xù)執(zhí)行代碼。
-
Debugger.resumed:Fired when the virtual machine resumed execution.
隨后代碼跳到下一行,此時(shí)又會(huì)收到 Debugger.paused消息。
點(diǎn)擊“Resume Script Execution” (按鈕 2)按鈕,Devtools 會(huì)收到 Debugger.resumed消息,如果還存在斷點(diǎn),則此時(shí)也會(huì)收到 Debugger.paused消息。
此外這里還有一個(gè) Overlay.setPausedInDebuggerMessage 消息,為 Devtools 發(fā)送給頁面,其信息主要是讓頁面展示代碼停止?fàn)顟B(tài)下應(yīng)該展示的消息,默認(rèn)為 {"message":"Paused in debugger"},也就是如下圖展示的內(nèi)容:
除了上面兩個(gè)按鈕,還有幾個(gè)調(diào)試按鈕,如下圖綠色區(qū)域內(nèi):
分別是:Step into next function call、Step out of current function、Step、Deactivate breakpoints。
????:
Step into next function call:這個(gè)按鈕用于進(jìn)入當(dāng)前行代碼所在的函數(shù)內(nèi)部,即單步進(jìn)入函數(shù)中執(zhí)行。 Step out of current function:這個(gè)按鈕用于跳出當(dāng)前函數(shù),即單步跳出當(dāng)前函數(shù)執(zhí)行。 Step:這個(gè)按鈕用于單步執(zhí)行代碼,即逐行執(zhí)行代碼。 Deactivate breakpoints:這個(gè)按鈕用于禁用所有的斷點(diǎn),即暫停調(diào)試器的所有斷點(diǎn)。
點(diǎn)擊“Step into next function call”,Devtools 會(huì)發(fā)送 Debugger.stepInto 消息,并收到 Debugger.resumed和 Debugger.paused消息,進(jìn)入到函數(shù)內(nèi)部。
-
Debugger.stepInto:Steps into the function call.
點(diǎn)擊“Step out of current function”,Devtools 會(huì)發(fā)送 Debugger.stepOut消息,并收到 Debugger.resumed和 Debugger.paused消息,跳出該函數(shù)。
點(diǎn)擊 “Step” 按鈕,Devtools 則發(fā)送 Debugger.stepInto,代碼執(zhí)行到下一行,每次點(diǎn)擊,都會(huì)發(fā)送 Debugger.stepInto消息。
點(diǎn)擊 “Deactivate (/?di??k.t?.ve?t/) breakpoints”,Devtools 則發(fā)送 Debugger.setBreakpointsActive 消息。如果當(dāng)前斷點(diǎn)狀態(tài)為執(zhí)行狀態(tài),則參數(shù)為 active: false,同時(shí)設(shè)置藍(lán)色小塊顏色為透明色。
重新執(zhí)行代碼,斷點(diǎn)調(diào)試能力失效。
再點(diǎn)擊一次,則參數(shù)為 active: true,斷點(diǎn)調(diào)試能力生效。
基本通信源碼
了解完相關(guān)斷點(diǎ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。
這里列舉幾個(gè)操作按鈕通信較多的 API。
pause() 的主要邏輯為 2 點(diǎn):
-
設(shè)置使頁面斷點(diǎn)暫停狀態(tài)為 ture。 -
發(fā)送 Debugger.paused消息到頁面。
stepInto() 的主要邏輯為:
-
拿到跳轉(zhuǎn)的 skipList,它是一個(gè)字符串?dāng)?shù)組,用于指定要跳過的函數(shù)名稱列。在操作調(diào)試按鈕時(shí),一般都是空數(shù)組。 -
發(fā)送 Debugger.stepInto消息到頁面。
其他 API 邏輯類似。
再分析一下 chromium /?kro?.mi.?m/ 中的斷點(diǎn)調(diào)試代碼邏輯。chromium 中發(fā)送 CDP 消息到 Devtools 的邏輯在 devtools_agent_host_impl中,而斷點(diǎn)調(diào)試邏輯在devtools_session文件中,通過 agent 的 DispatchProtocolMessage最后調(diào)用到 session 的 shoulSendOnIO函數(shù)。
具體來說,這個(gè)函數(shù)接收一個(gè)包含 CDP 方法的 span 參數(shù),然后檢查該方法是否屬于一組特定的方法,如果是,則返回 true,表示該 CDP 消息需要轉(zhuǎn)發(fā)。
DevToolsSession 是 Chromium 源碼中的一個(gè)類,代表一個(gè) DevTools 會(huì)話。DevToolsSession 負(fù)責(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ǎn)調(diào)試方法,最后都會(huì)調(diào)用 DispatchToAgent方法,并走到 ShouldSendOnIO邏輯。
命中斷點(diǎn)
通過上面的分析,了解到了調(diào)試器和頁面之間的 CDP 通信內(nèi)容和 API 的基本實(shí)現(xiàn)。那 chromium 又是如何停止代碼到斷點(diǎn)的呢?為何可以停止代碼執(zhí)行呢?
在 DevTools 中,停止代碼執(zhí)行到斷點(diǎn)的核心實(shí)現(xiàn)是通過使用 V8 JS 引擎中的斷點(diǎn)機(jī)制來實(shí)現(xiàn)的。當(dāng) chromium 執(zhí)行到一個(gè)斷點(diǎn)時(shí),V8 會(huì)暫停 JS 代碼的執(zhí)行,并將控制權(quán)轉(zhuǎn)交給 Devtools。這時(shí)候,Devtools 可以執(zhí)行上述提到的斷點(diǎn)調(diào)試的各種操作。
這塊邏輯的代碼在 chromium
auction_v8_devtools_agent和auction_v8_devtools_session中,看起來比較復(fù)雜,涉及到 AuctionV8DevToolsSession 和 AuctionV8DevToolsAgent 兩個(gè)類,我的理解是 DevtoolsAgent 提供了一些 Devtools debugger 的服務(wù),并找到對(duì)應(yīng)的 DevtoolsSession 進(jìn)行通信。V8 將 ws 格式信息轉(zhuǎn)交給了 DevtoolsSession,最后通過 DevtoolsAgent 發(fā)送到了 Devtools。
大概邏輯如下:
通過 Devtools Agent,負(fù)責(zé)接收 Devtools 通信信息,并將斷點(diǎn)信息移交給 V8,然后由 V8 來對(duì)代碼進(jìn)行停止操作。
V8 里面的邏輯我只能看一個(gè)大概,整體邏輯如下:
V8Debugger 是一個(gè)抽象,V8DebuggerAgentImpl 類實(shí)現(xiàn)了這個(gè)類,它是 Debug 類和 V8 調(diào)試協(xié)議之間的中介,負(fù)責(zé)將調(diào)試消息轉(zhuǎn)換為 V8 調(diào)試協(xié)議中定義的格式。
關(guān)于 V8 斷點(diǎn) Debugger 更底層的邏輯是與 os、cpu 相關(guān),os 提供了系統(tǒng)調(diào)用來實(shí)現(xiàn)可執(zhí)行代碼的中斷。
中斷則是 cpu 執(zhí)行下一條指令之前,關(guān)注一下中斷標(biāo)記,從而判斷是否需要中斷執(zhí)行。整體邏輯上對(duì)照著 Vue 的渲染原理即可,每次事件循環(huán)結(jié)束后最后去走一次渲染 DOM。
V8 本身也是將 JS 轉(zhuǎn)為可執(zhí)行語言,這也就是為何 JS 可以在瀏覽器中擁有斷點(diǎn)能力了。
這里涉及到一些指令操作,沒有深究。
同時(shí),V8 中斷代碼執(zhí)行,也會(huì)提供一些環(huán)境數(shù)據(jù)到 Devtools,譬如當(dāng)前變量數(shù)值等,這時(shí)候 V8 就會(huì)將這些調(diào)試信息通過 V8 Debug Protocol 協(xié)議的格式丟給 Debug,最后丟給 Devtools,從而鼠標(biāo)懸浮在 sources panel 即可看到對(duì)應(yīng)的數(shù)據(jù)內(nèi)容。
Debugger.evaluateOnCallFrame 和 Runtime.getProperties 可以拿到一些環(huán)境信息,前者比如一些 number 數(shù)字就可以得到。
Vscode Web 代碼斷點(diǎn)調(diào)試原理
在 Vscode 中調(diào)試代碼,能讓開發(fā)者專注于代碼本身,一邊開發(fā)運(yùn)行一邊斷點(diǎn)調(diào)試查看變量信息,并減少一些臟代碼的開發(fā)。如下圖所示,可以看到,似乎是將瀏覽器的 Debugger 的邏輯照搬到了 Vscode 中。
在介紹完瀏覽器斷點(diǎn)調(diào)試的邏輯以后,我們大概了解了頁面與 Devtools 的通信過程和相關(guān) CDP 信息。有了這些基礎(chǔ),我們?cè)俜治龇治?Vscode 中是如何實(shí)現(xiàn)斷點(diǎn)調(diào)試 Web 代碼的。
launch.json
在 Vscode 中配置調(diào)試后,會(huì)生成一個(gè) .vscode/launch.json 文件,其主要是配置需要調(diào)試的 url 和遠(yuǎn)程調(diào)試的端口號(hào) port。
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "針對(duì) localhost 啟動(dòng) Chrome",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
}
]
}
Debugging Architecture of VS Code
[?ɑrk??tekt??r]
Vscode 并不只是前端開發(fā)者調(diào)試 JS 使用,還可以調(diào)試其他語言,Python 一些教程就建議使用 Vscode 調(diào)試。因此 Vscode 的調(diào)試架構(gòu)高度靈活,可以支持多種編程語言和調(diào)試場(chǎng)景,并且可以基于該架構(gòu)實(shí)現(xiàn)各種調(diào)試擴(kuò)展。
如上圖,Vscode 的調(diào)試架構(gòu)中,有 3 個(gè) Core Module:
-
Debug Adapter:調(diào)試適配器是 Vscode 和具體調(diào)試目標(biāo)之間的橋梁。適配器主要就是負(fù)責(zé)將調(diào)試請(qǐng)求轉(zhuǎn)換為調(diào)試目標(biāo),并將調(diào)試目標(biāo)轉(zhuǎn)為調(diào)試器需要的結(jié)果,其通過 Vscode Debug Protocol 協(xié)議通信。Debug Adapter 提供了一組標(biāo)準(zhǔn)的調(diào)試接口,包括設(shè)置斷點(diǎn)、單步執(zhí)行、查看變量值等。 -
Debug Extension:調(diào)試擴(kuò)展是 Vscode 內(nèi)置的插件,提供特定語言或者場(chǎng)景的實(shí)現(xiàn)。比如可以調(diào)試 JS TS Python 等,同時(shí)社區(qū)也可以提供相關(guān)擴(kuò)展,譬如 Java:
-
Debug UI:即 Vscode 的操作界面,它提供了調(diào)試器的各種操作和功能,例如設(shè)置斷點(diǎn)、單步調(diào)試、查看變量值等。
????:別忘了另外一個(gè) Debugger,即為 launch.json 中的 type,指底層的調(diào)試目標(biāo),例如 Node.js 運(yùn)行時(shí)、Chrome 瀏覽器等等。比如斷點(diǎn)后的信息需要傳遞給 chrome,需要去暫定代碼執(zhí)行,并斷點(diǎn)逐步執(zhí)行等。
原理
在了解原理之前,先看一些現(xiàn)象:
-
當(dāng) Vscode 啟動(dòng)調(diào)試并走到指定斷點(diǎn)時(shí),Chrome 自身調(diào)試器也會(huì)走到對(duì)應(yīng)的調(diào)試邏輯(Devtools 本身也是一個(gè) ws client,任何 client 都會(huì)收到 chrome 的 cdp 消息)。
-
當(dāng)在 Vscode 調(diào)試面板操作 StepInto按鈕時(shí),Vscode 代碼會(huì)走到下一步,同時(shí) chrome 調(diào)試也會(huì)走到下一步。 -
當(dāng)在 Chrome Devtools 中操作 StepInto按鈕時(shí),Chrome Devtools 代碼也會(huì)走到下一步,同時(shí) Vscode 中代碼也會(huì)跳轉(zhuǎn)到下一步。
通過上面 3 種現(xiàn)象可以看出,Vscode Webpage Devtools 關(guān)系如下:
細(xì)品一下,這時(shí)候就可以知道為何需要 Debug Adapter 了。實(shí)際上,就是將 CDP 消息轉(zhuǎn)為 DAP。
Workflow
Vscode Chrome Debug 的工作流程如下:
-
Vscode 啟動(dòng) JavaScript 調(diào)試器,并配置調(diào)試器相關(guān)的參數(shù),例如調(diào)試類型、調(diào)試目標(biāo)等等。 -
JavaScript Debug Extension 會(huì)根據(jù)配置啟動(dòng)一個(gè) Debug Adapter 進(jìn)程,并向該進(jìn)程發(fā)送調(diào)試請(qǐng)求,請(qǐng)求 Debug Adapter 與 Debugger 之間建立連接。 -
Debug Adapter 進(jìn)程會(huì)根據(jù)用戶的配置,啟動(dòng)相應(yīng)的 Chrome,并與對(duì)應(yīng)網(wǎng)頁(Debugger)建立連接。 -
Debug Adapter 進(jìn)程會(huì)將調(diào)試結(jié)果轉(zhuǎn)換為 Vscode 支持的調(diào)試消息格式,即上面提到的 DAP,并將調(diào)試消息發(fā)送給 Vscode。
這里的核心就是 Extension,其作用就是調(diào)度與控制,比如啟動(dòng) Adapter 進(jìn)程,發(fā)送與接收調(diào)試信息等等,屬于大 BOSS,而 Adapter 只是下屬。
JS Debug Extension
上面提到,chromium 內(nèi)部是使用 CDP 協(xié)議通信,因此 Extension 想要正確調(diào)試 Chrome WebPage,首先就得遵守 Chrome 的玩法。比如,在 Vscode 中點(diǎn)擊 StepInto 按鈕,這時(shí)候會(huì)將對(duì)應(yīng)操作信息轉(zhuǎn)化為 CDP 信息,然后再發(fā)送給 WebPage。
Extension 啟動(dòng) 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)部實(shí)現(xiàn)與開發(fā)者工具調(diào)試器和模擬器的通信相似, Extension 與 WebPage 通信也是拿到了頁面的 debug ws url,在 Extension 內(nèi)部創(chuàng)建一個(gè) ws client,通過該 client 監(jiān)聽來自于 WebPage CDP 信息,并轉(zhuǎn)發(fā)到會(huì)話的 Adapter,最后再交給 Vscode。
看最新的代碼,JS Debug Extension 也會(huì)負(fù)責(zé)部分調(diào)試 UI 相關(guān)邏輯。
Command 實(shí)例
以 StepInto舉例,在 Vscode 中點(diǎn)擊該按鈕以后,會(huì)發(fā)送一個(gè) 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 中查看實(shí)時(shí)的 DAP 和 CDP 消息,可以通過如下操作:
源碼調(diào)試
上面給到的例子非常簡(jiǎn)單,js 代碼也沒有經(jīng)過構(gòu)建生成編譯后的代碼。但是實(shí)際場(chǎng)景中開發(fā)的項(xiàng)目會(huì)引入各種開源庫,然后經(jīng)過諸如 Webpack 等打包構(gòu)建工具做編譯打包,才能在瀏覽器中運(yùn)行。編譯壓縮后的代碼一般不具備可讀性,因此在編譯后代碼進(jìn)行調(diào)試成本比較高。
We all know,SourceMap 存儲(chǔ)著源碼和生產(chǎn)代碼之間的映射關(guān)系。譬如我這里啟動(dòng)了一個(gè) Vite 項(xiàng)目:
當(dāng)我在源碼的 main.ts 中設(shè)置斷點(diǎn)時(shí),可以看到 Request 中的 url 為 host:port/src/main.ts,即實(shí)際傳給 WebPage 的斷點(diǎn)文件為編譯后的文件。
JS Debug Extension 亦是如此。
當(dāng)在 Vscode 的源碼中增加了一個(gè)斷點(diǎn),JS Debug Extension 會(huì)根據(jù) sourceMap 將源代碼路徑映射到編譯后的代碼路徑中,并將這個(gè)信息發(fā)送給瀏覽器。
所以呀,解析是前端行為。
擴(kuò)展:SourceMap 加載
SourceMap 雖然也是靜態(tài)資源,但是其加載在 Network 面板并不能看到,而是在 Developer Resources 中。
為了啟動(dòng)快,我用的 Vite 來生成項(xiàng)目。Vite 利用了瀏覽器原生的 ES modules 功能,根據(jù)文件依賴關(guān)系,生成依賴樹,然后各模塊文件模塊單獨(dú)加載。Vite 文件都有單獨(dú)的 SourceMap,不需要配 SourceMap 依賴。
可以看到,這里 Vite 默認(rèn)是直接內(nèi)嵌的 SourceMap,無需單獨(dú)請(qǐng)求, 可以在代碼文件加載完成后,就直接解析了,紅框里面展示的鏈接就是 Base64 的形式了。
??SourceMap 的解析是交給 Devtools 本身的,Debugger 只負(fù)責(zé)運(yùn)行和暫停。因此,如果斷點(diǎn)在 SourceMap 解析完成之前觸發(fā),則沒法告訴 Debugger 正確的地址,可能會(huì)出現(xiàn)斷點(diǎn)無效情況。
IDE 小程序斷點(diǎn)調(diào)試
Devtools Debug
根據(jù)上面的介紹,小程序斷點(diǎn)調(diào)試的最簡(jiǎn)單辦法就是在代碼中寫上 debugger,然后交給 v8 處理即可。另外還有一種方式就是打開小程序調(diào)試器,在 sources panel 中打斷點(diǎn),如下圖:
打斷點(diǎn),刷新小程序,即可跳轉(zhuǎn)到斷點(diǎn)位置。此時(shí)可以看到對(duì)應(yīng)的 CDP 消息中的 Request。
可以看到,這里點(diǎn)擊的是 56 行,但實(shí)際上 Request 中卻不是,Devtools 通過 sourceMap 進(jìn)行了處理,定位到了 64 行。根據(jù)上面提到的源碼調(diào)試邏輯,這里的位置為編譯后的代碼位置,找到編譯產(chǎn)物代碼 app.js 即可看到 real position。
IDE Editor Debug???
考慮到上面提到的 Vscode 有 web 斷點(diǎn)調(diào)試能力,那 IDE Editor 或許也是可以支持?jǐn)帱c(diǎn)調(diào)試能力的。
Vscode 可以直接在編輯器運(yùn)行項(xiàng)目,然后啟動(dòng)自定義的調(diào)試目標(biāo)(Debugger)。
IDE 為小程序運(yùn)行時(shí)的載體,與 Vscode 啟動(dòng) web 項(xiàng)目不一樣,其邏輯為編譯完成后生成一個(gè)編譯產(chǎn)物目錄,通過靜態(tài)服務(wù),Simulator 直接加載對(duì)應(yīng)編譯產(chǎn)物。因此,IDE 的 Editor 實(shí)際上跟 Simulator 沒什么聯(lián)系的。
假設(shè)借用 Devtools Debug 的邏輯,當(dāng)在 Editor 打斷點(diǎn)時(shí),捕獲所有的斷點(diǎn) DAP 消息,當(dāng)開啟調(diào)試時(shí),刷新模擬器,將所有的斷點(diǎn)信息轉(zhuǎn)為 CDP 信息發(fā)送給模擬器,或許就可以簡(jiǎn)單實(shí)現(xiàn)該能力。
當(dāng)然,考慮到是在源碼中打斷點(diǎn),這里的難點(diǎn)應(yīng)該是在于要實(shí)現(xiàn) sourceMap 解析,而 Debug UI 則可以利用 Vscode JS Extension,或者通過自定義實(shí)現(xiàn)一個(gè) Debug UI。
總結(jié)
本文從抖音開發(fā)者工具支持?jǐn)帱c(diǎn)調(diào)試能力需求引入,概述了瀏覽器斷點(diǎn)調(diào)試的基本原理,也介紹了 Vscode Web 代碼斷點(diǎn)調(diào)試能力,詳細(xì)介紹了各模塊中各 CDP 消息通信邏輯。閱讀本文可以掌握前端各種調(diào)試方法的基本原理。
加入我們
抖音開放平臺(tái)提供小程序、移動(dòng)應(yīng)用、網(wǎng)站應(yīng)用、直播小玩法等多業(yè)務(wù)載體,為開發(fā)者提供豐富的能力和解決方案。抖音開放平臺(tái)基于平臺(tái)規(guī)則和開發(fā)者訴求,提供了兩種開放模式:能力開放和行業(yè)開放。
在能力開放方面,平臺(tái)提供了運(yùn)營(yíng)抖音號(hào)、頭條號(hào)的相關(guān)能力,如發(fā)布視頻和獲取視頻數(shù)據(jù)、用戶數(shù)據(jù)和各類榜單信息等。同時(shí)也圍繞抖音小程序提供了擔(dān)保支付、客服管理、訂閱消息等基礎(chǔ)能力,以及短視頻 / 直播間掛載小程序、流量主、廣告主等經(jīng)營(yíng)能力。
在行業(yè)開放方面,平臺(tái)基于挖掘行業(yè)痛點(diǎn)、解決實(shí)際問題角度出發(fā),提供了到店餐飲、機(jī)酒旅、泛知識(shí)等多行業(yè)解決方案,提升品牌及商家的經(jīng)營(yíng)收入和行業(yè)競(jìng)爭(zhēng)力。
點(diǎn)擊上方關(guān)注 · 我們下期再見
參考文檔
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


