頭條創(chuàng)作挑戰(zhàn)賽#
本文不同本人掘金平臺的文章:https://juejin.cn/post/6875899284961787911
直接上干貨!
在控制臺上展示數(shù)組或?qū)ο螅褂胏onsole.table比console.log更加直觀明了。
// 在控制臺上運(yùn)行
console.table([
{ firstName: 'John', lastName: 'Doe', age: 2 },
{ firstName: 'William', lastName: 'Shakespeare', age: 3 }
])
展示為一個table表,友好很多:
當(dāng)然,你還可以指定展示哪些列~
// 在控制臺上運(yùn)行
console.table([
{ firstName: 'John', lastName: 'Doe', age: 2 },
{ firstName: 'William', lastName: 'Shakespeare', age: 3 }
], ['firstName', 'lastName'])
上面指定展示firstName和lastName這兩列,當(dāng)然,(index) 是默認(rèn)有的。
注意?:語法 console.table(data [, columns]);
如果你使用谷歌瀏覽器并需要復(fù)制控制臺輸出的數(shù)據(jù)。你可以使用copy()命令行,而不是手動高亮選擇對應(yīng)代碼進(jìn)行復(fù)制。
const data=[2, 3, 4];
copy(data);
執(zhí)行上面的代碼,會將data數(shù)據(jù)值復(fù)制到你的粘貼板上。你可以在任意文檔中進(jìn)行粘貼。
注意?:copy命令僅在谷歌瀏覽器控制臺上生效,并且在node.js環(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ā)小程序,我也是負(fù)責(zé)本地開發(fā)能力的建設(shè)。
因?yàn)楣ぷ髟蜃罱鼘帱c(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í)踐。
作為一個前端開發(fā),前端調(diào)試的方式一般有如下幾種:
相比于 console,debugger 可以看到代碼實(shí)際的執(zhí)行路線以及每個變量的變化,代碼可以跳著看,也可以針對某個函數(shù)步步執(zhí)行。
但是 console 與 debugger 方式對代碼都有侵入,在開發(fā)階段可能要不斷增加和移除來調(diào)試,如果不小心忘了,那 mr 又得打回并重新提交了…
相信很多人在提 mr 都有類似經(jīng)驗(yàn)…
相對來說,瀏覽器中找到 source 源碼打斷點(diǎn)是一個更好的方式,但是還是需要打開 Devtools ,并在 sources 面板找到文件注入斷點(diǎn),操作上也是有點(diǎn)小麻煩。
因此第 3 種方式,可能是不錯的方式,在 vscode 中直接在源碼中調(diào)試,并能看到具體的變量信息和網(wǎng)頁效果。
實(shí)際上,瀏覽器打斷點(diǎn)與在 vscode 打斷點(diǎn)本質(zhì)原理都類似。下面就聊一聊瀏覽器斷點(diǎn)調(diào)試和 vscode 斷點(diǎn)調(diào)試的原理。
在了解具體場景之前,首先有一個比較重要的概念,那就是 CDP。
CDP(Chrome DevTools Protocol)是一種通過網(wǎng)絡(luò)協(xié)議與 Google Chrome 或其他兼容的瀏覽器進(jìn)行通信的協(xié)議。通過 CDP,開發(fā)者可以遠(yuǎn)程控制瀏覽器,獲取瀏覽器狀態(tài)信息,以及執(zhí)行各種瀏覽器操作,從而實(shí)現(xiàn)自動化測試、性能分析、調(diào)試等應(yīng)用場景。
:
CDP 最早于 2011 年在 Chrome 15 版本中引入,作為 Chrome DevTools 的核心組件之一而出現(xiàn)。在此之前,開發(fā)者通常需要通過瀏覽器插件或者第三方工具來進(jìn)行調(diào)試和測試,這些工具通常不夠標(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ā)展推動了 Web 開發(fā)和測試的發(fā)展,為開發(fā)者帶來了更加高效和便捷的開發(fā)和測試方式。
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 信息包括:
這幾個也是平常開發(fā)中最常用到的幾個 Domains 了。
chrome 的 Devtools (Front-End Devtools)與 Web Page 之間的調(diào)試也是通過 CDP 通信的,如下圖所示:
除了調(diào)試,CDP 額外應(yīng)用場景也很多,比如剛才提到的自動化測試,通過 CDP 模擬用戶行為,操作頁面元素等,或者 CDP 獲取瀏覽器的性能指標(biāo)生成性能報(bào)告,還可以通過 CDP 模擬瀏覽器行為,獲取頁面數(shù)據(jù),實(shí)現(xiàn)爬蟲等等。
帶著問題出發(fā),可能需要搞懂以下 3 點(diǎn):
頁面與 Devtools 是如何通信的?
斷點(diǎn)操作邏輯通信過程是什么?
如何實(shí)現(xiàn)命中斷點(diǎn)并停止代碼執(zhí)行的?
在瀏覽器中,網(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 信息。
為什么會發(fā)送多次,我也不理解,內(nèi)容基本上是一致的。
點(diǎn)擊斷點(diǎn)以后,主要有以下一些 CDP 消息在頁面與 Devtools 之間通信:
setBreakpointsActive 表示告訴頁面要設(shè)置一個調(diào)試斷點(diǎn)了;setBreakpointByUrl 則是告訴頁面設(shè)置的具體信息;getPossibleBreakpoints 表示設(shè)置以后獲取正確的斷點(diǎn)位置,并展示藍(lán)色小塊。
有時候可能會發(fā)現(xiàn)設(shè)置了某一行為斷點(diǎn),但是斷點(diǎn)的位置并不是指向的位置,而是另外的位置。比如上面截圖,如果在 15 行設(shè)置斷點(diǎn),則最后展示斷點(diǎn)位置為 18 行。
整體流程如下圖:
除了在 sources 面板增加斷點(diǎn),還可以取消斷點(diǎn)。取消斷點(diǎn)的 CDP 非常簡單, Devtools 會給 Web Page 發(fā)送一個 Debugger.removeBreakpoint 來移除斷點(diǎn)。
當(dāng)點(diǎn)擊完斷點(diǎn)以后,頁面會走到斷點(diǎn)所在的代碼位置,同時 Devtools 會接收到一些 CDP 消息,通知它當(dāng)前斷點(diǎn)的狀態(tài)和上下文信息。
我寫了一個實(shí)例,是關(guān)于數(shù)字的增減邏輯,并在數(shù)字增加的時候,走到斷點(diǎn)位置(不需要刷新頁面)。
可以看到,當(dāng)點(diǎn)擊 + 號以后,頁面就進(jìn)入斷點(diǎn)調(diào)試邏輯,此時 Devtools 會收到 Debugger.paused消息:
此時表示頁面已經(jīng)暫停了代碼執(zhí)行,Devtools 可以通過 Debugger.paused事件中的參數(shù),獲取當(dāng)前斷點(diǎn)的上下文信息,如斷點(diǎn)所在的函數(shù)、變量值、堆棧信息等。
具體信息沒有對應(yīng)看
點(diǎn)擊“Step Over next function call”(按鈕 1),Devtools 會收到 Debugger.resumed r??zu?m d 消息,通知繼續(xù)執(zhí)行代碼。
隨后代碼跳到下一行,此時又會收到 Debugger.paused消息。
點(diǎn)擊“Resume Script Execution” (按鈕 2)按鈕,Devtools 會收到 Debugger.resumed消息,如果還存在斷點(diǎn),則此時也會收到 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:這個按鈕用于進(jìn)入當(dāng)前行代碼所在的函數(shù)內(nèi)部,即單步進(jìn)入函數(shù)中執(zhí)行。
Step out of current function:這個按鈕用于跳出當(dāng)前函數(shù),即單步跳出當(dāng)前函數(shù)執(zhí)行。
Step:這個按鈕用于單步執(zhí)行代碼,即逐行執(zhí)行代碼。
Deactivate breakpoints:這個按鈕用于禁用所有的斷點(diǎn),即暫停調(diào)試器的所有斷點(diǎn)。
點(diǎn)擊“Step into next function call”,Devtools 會發(fā)送 Debugger.stepInto 消息,并收到 Debugger.resumed和 Debugger.paused消息,進(jìn)入到函數(shù)內(nèi)部。
點(diǎn)擊“Step out of current function”,Devtools 會發(fā)送 Debugger.stepOut消息,并收到 Debugger.resumed和 Debugger.paused消息,跳出該函數(shù)。
點(diǎn)擊 “Step” 按鈕,Devtools 則發(fā)送 Debugger.stepInto,代碼執(zhí)行到下一行,每次點(diǎn)擊,都會發(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è)置藍(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。
這里列舉幾個操作按鈕通信較多的 API。
pause() 的主要邏輯為 2 點(diǎn):
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ù)。
具體來說,這個函數(shù)接收一個包含 CDP 方法的 span 參數(shù),然后檢查該方法是否屬于一組特定的方法,如果是,則返回 true,表示該 CDP 消息需要轉(zhuǎn)發(fā)。
DevToolsSession 是 Chromium 源碼中的一個類,代表一個 DevTools 會話。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)試方法,最后都會調(diào)用 DispatchToAgent方法,并走到 ShouldSendOnIO邏輯。
通過上面的分析,了解到了調(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í)行到一個斷點(diǎn)時,V8 會暫停 JS 代碼的執(zhí)行,并將控制權(quán)轉(zhuǎn)交給 Devtools。這時候,Devtools 可以執(zhí)行上述提到的斷點(diǎn)調(diào)試的各種操作。
這塊邏輯的代碼在 chromium auction_v8_devtools_agent 和 auction_v8_devtools_session 中,看起來比較復(fù)雜,涉及到 AuctionV8DevToolsSession 和 AuctionV8DevToolsAgent 兩個類,我的理解是 DevtoolsAgent 提供了一些 Devtools debugger 的服務(wù),并找到對應(yīng)的 DevtoolsSession 進(jìn)行通信。V8 將 ws 格式信息轉(zhuǎn)交給了 DevtoolsSession,最后通過 DevtoolsAgent 發(fā)送到了 Devtools。
大概邏輯如下:
通過 Devtools Agent,負(fù)責(zé)接收 Devtools 通信信息,并將斷點(diǎn)信息移交給 V8,然后由 V8 來對代碼進(jìn)行停止操作。
V8 里面的邏輯我只能看一個大概,整體邏輯如下:
V8Debugger 是一個抽象,V8DebuggerAgentImpl 類實(shí)現(xiàn)了這個類,它是 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í)行。整體邏輯上對照著 Vue 的渲染原理即可,每次事件循環(huán)結(jié)束后最后去走一次渲染 DOM。
V8 本身也是將 JS 轉(zhuǎn)為可執(zhí)行語言,這也就是為何 JS 可以在瀏覽器中擁有斷點(diǎn)能力了。
這里涉及到一些指令操作,沒有深究。
同時,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ā)運(yùn)行一邊斷點(diǎn)調(diào)試查看變量信息,并減少一些臟代碼的開發(fā)。如下圖所示,可以看到,似乎是將瀏覽器的 Debugger 的邏輯照搬到了 Vscode 中。
在介紹完瀏覽器斷點(diǎn)調(diào)試的邏輯以后,我們大概了解了頁面與 Devtools 的通信過程和相關(guān) CDP 信息。有了這些基礎(chǔ),我們再分析分析 Vscode 中是如何實(shí)現(xiàn)斷點(diǎn)調(diào)試 Web 代碼的。
在 Vscode 中配置調(diào)試后,會生成一個 .vscode/launch.json 文件,其主要是配置需要調(diào)試的 url 和遠(yuǎn)程調(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)實(shí)現(xiàn)各種調(diào)試擴(kuò)展。
如上圖,Vscode 的調(diào)試架構(gòu)中,有 3 個 Core Module:
:別忘了另外一個 Debugger,即為 launch.json 中的 type,指底層的調(diào)試目標(biāo),例如 Node.js 運(yùn)行時、Chrome 瀏覽器等等。比如斷點(diǎn)后的信息需要傳遞給 chrome,需要去暫定代碼執(zhí)行,并斷點(diǎn)逐步執(zhí)行等。
在了解原理之前,先看一些現(xiàn)象:
通過上面 3 種現(xiàn)象可以看出,Vscode Webpage Devtools 關(guān)系如下:
細(xì)品一下,這時候就可以知道為何需要 Debug Adapter 了。實(shí)際上,就是將 CDP 消息轉(zhuǎn)為 DAP。
Vscode Chrome Debug 的工作流程如下:
這里的核心就是 Extension,其作用就是調(diào)度與控制,比如啟動 Adapter 進(jìn)程,發(fā)送與接收調(diào)試信息等等,屬于大 BOSS,而 Adapter 只是下屬。
上面提到,chromium 內(nèi)部是使用 CDP 協(xié)議通信,因此 Extension 想要正確調(diào)試 Chrome WebPage,首先就得遵守 Chrome 的玩法。比如,在 Vscode 中點(diǎn)擊 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)部實(shí)現(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 也會負(fù)責(zé)部分調(diào)試 UI 相關(guān)邏輯。
以 StepInto舉例,在 Vscode 中點(diǎn)擊該按鈕以后,會發(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 中查看實(shí)時的 DAP 和 CDP 消息,可以通過如下操作:
上面給到的例子非常簡單,js 代碼也沒有經(jīng)過構(gòu)建生成編譯后的代碼。但是實(shí)際場景中開發(fā)的項(xiàng)目會引入各種開源庫,然后經(jīng)過諸如 Webpack 等打包構(gòu)建工具做編譯打包,才能在瀏覽器中運(yùn)行。編譯壓縮后的代碼一般不具備可讀性,因此在編譯后代碼進(jìn)行調(diào)試成本比較高。
We all know,SourceMap 存儲著源碼和生產(chǎn)代碼之間的映射關(guān)系。譬如我這里啟動了一個 Vite 項(xiàng)目:
當(dāng)我在源碼的 main.ts 中設(shè)置斷點(diǎn)時,可以看到 Request 中的 url 為 host:port/src/main.ts,即實(shí)際傳給 WebPage 的斷點(diǎn)文件為編譯后的文件。
JS Debug Extension 亦是如此。
當(dāng)在 Vscode 的源碼中增加了一個斷點(diǎn),JS Debug Extension 會根據(jù) sourceMap 將源代碼路徑映射到編譯后的代碼路徑中,并將這個信息發(fā)送給瀏覽器。
所以呀,解析是前端行為。
SourceMap 雖然也是靜態(tài)資源,但是其加載在 Network 面板并不能看到,而是在 Developer Resources 中。
為了啟動快,我用的 Vite 來生成項(xiàng)目。Vite 利用了瀏覽器原生的 ES modules 功能,根據(jù)文件依賴關(guān)系,生成依賴樹,然后各模塊文件模塊單獨(dú)加載。Vite 文件都有單獨(dú)的 SourceMap,不需要配 SourceMap 依賴。
可以看到,這里 Vite 默認(rèn)是直接內(nèi)嵌的 SourceMap,無需單獨(dú)請求, 可以在代碼文件加載完成后,就直接解析了,紅框里面展示的鏈接就是 Base64 的形式了。
??SourceMap 的解析是交給 Devtools 本身的,Debugger 只負(fù)責(zé)運(yùn)行和暫停。因此,如果斷點(diǎn)在 SourceMap 解析完成之前觸發(fā),則沒法告訴 Debugger 正確的地址,可能會出現(xiàn)斷點(diǎn)無效情況。
根據(jù)上面的介紹,小程序斷點(diǎn)調(diào)試的最簡單辦法就是在代碼中寫上 debugger,然后交給 v8 處理即可。另外還有一種方式就是打開小程序調(diào)試器,在 sources panel 中打斷點(diǎn),如下圖:
打斷點(diǎn),刷新小程序,即可跳轉(zhuǎn)到斷點(diǎn)位置。此時可以看到對應(yīng)的 CDP 消息中的 Request。
可以看到,這里點(diǎn)擊的是 56 行,但實(shí)際上 Request 中卻不是,Devtools 通過 sourceMap 進(jìn)行了處理,定位到了 64 行。根據(jù)上面提到的源碼調(diào)試邏輯,這里的位置為編譯后的代碼位置,找到編譯產(chǎn)物代碼 app.js 即可看到 real position。
考慮到上面提到的 Vscode 有 web 斷點(diǎn)調(diào)試能力,那 IDE Editor 或許也是可以支持?jǐn)帱c(diǎn)調(diào)試能力的。
Vscode 可以直接在編輯器運(yùn)行項(xiàng)目,然后啟動自定義的調(diào)試目標(biāo)(Debugger)。
IDE 為小程序運(yùn)行時的載體,與 Vscode 啟動 web 項(xiàng)目不一樣,其邏輯為編譯完成后生成一個編譯產(chǎn)物目錄,通過靜態(tài)服務(wù),Simulator 直接加載對應(yīng)編譯產(chǎn)物。因此,IDE 的 Editor 實(shí)際上跟 Simulator 沒什么聯(lián)系的。
假設(shè)借用 Devtools Debug 的邏輯,當(dāng)在 Editor 打斷點(diǎn)時,捕獲所有的斷點(diǎn) DAP 消息,當(dāng)開啟調(diào)試時,刷新模擬器,將所有的斷點(diǎn)信息轉(zhuǎn)為 CDP 信息發(fā)送給模擬器,或許就可以簡單實(shí)現(xiàn)該能力。
當(dāng)然,考慮到是在源碼中打斷點(diǎn),這里的難點(diǎn)應(yīng)該是在于要實(shí)現(xiàn) sourceMap 解析,而 Debug UI 則可以利用 Vscode JS Extension,或者通過自定義實(shí)現(xiàn)一個 Debug UI。
本文從抖音開發(fā)者工具支持?jǐn)帱c(diǎn)調(diào)試能力需求引入,概述了瀏覽器斷點(diǎn)調(diào)試的基本原理,也介紹了 Vscode Web 代碼斷點(diǎn)調(diào)試能力,詳細(xì)介紹了各模塊中各 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
術(shù)領(lǐng)域總是充滿著神秘的未知和挑戰(zhàn),有趣又令人不能自拔。就像 JavaScript,即使是每天使用它進(jìn)行開發(fā)交互的開發(fā)人員,而語言的某些部分仍然未被開發(fā)。
了解工具可以使工具最大限度的幫助你完成任務(wù)。盡管JavaScript的調(diào)試非常麻煩,但在掌握了技巧 (tricks) 的情況下,依然可以用盡量少的的時間解決這些錯誤 (errors) 和問題 (bugs) 。
下面小編列出了14個關(guān)于JavaScript的調(diào)試技巧。雖然調(diào)試技巧也可以用在別的檢查工具上,但大部分的技巧還是用在 Chrome Inspector 和 Firefox 上的。
‘debugger’ 是 console.log 之外最好的調(diào)試工具,簡單暴力。只要把它寫到代碼里,Chrome 運(yùn)行的時候就會自動自動停在那。甚至可以用條件語句把它包裹起來,這樣就可以在需要的時候才執(zhí)行它。也是一款非常好用的調(diào)試工具了。
if (thisThing) {
debugger;
}
很多的時候,你可能會有一堆對象需要查看。可以用console.log把每一個對象都輸出出來,也可以用console.table語句直接把所有的對象都直接輸出成為一個表格。demo如下:
var animals=[
{ animal: 'Horse', name: 'Henry', age: 43 },
{ animal: 'Dog', name: 'Fred', age: 13 },
{ animal: 'Cat', name: 'Frodo', age: 18 }
];
console.table(animals);
輸出結(jié)果:
雖然把各種各樣的手機(jī)都擺在桌子上看起來很酷,但卻很不現(xiàn)實(shí)。但是,瀏覽器內(nèi)卻提供了你所需要的一切。進(jìn)入檢查面板點(diǎn)擊“切換設(shè)備模式”按鈕。這樣,就可以在窗口內(nèi)調(diào)整視窗的大小。
在元素面板上標(biāo)記一個 DOM 元素并在 concole 中使用它。Chrome Inspector 的歷史記錄保存最近的五個元素,最后被標(biāo)記的元素記為 >在元素面板上標(biāo)記一個 DOM 元素并在 concole 中使用它。Chrome Inspector 的歷史記錄保存最近的五個元素,最后被標(biāo)記的元素記為 $0,倒數(shù)第二個被標(biāo)記的記為 $1,以此類推。<,倒數(shù)第二個被標(biāo)記的記為 ,以此類推。
如果你像下面那樣把元素按順序標(biāo)記為 ‘item-4′, ‘item-3’, ‘item-2’, ‘item-1’, ‘item-0’ ,你就可以在 concole 中獲取到 DOM 節(jié)點(diǎn):
當(dāng)你想知道某些代碼的執(zhí)行時間的時候這個工具將會非常有用,特別是當(dāng)你定位很耗時的循環(huán)的時候。你甚至可以通過標(biāo)簽來設(shè)置多個 timer 。demo 如下:
console.time('Timer1');
var items=[];
for(var i=0; i < 100000; i++){
items.push({index: i});
}
console.timeEnd('Timer1');
運(yùn)行結(jié)果:
在使用JavaScript框架的時候會產(chǎn)生很多的代碼。
它創(chuàng)建視圖觸發(fā)事件而且最終會想知道函數(shù)調(diào)用是怎么發(fā)生的。
因?yàn)?JavaScript 不是一個很結(jié)構(gòu)化的語言,有時候很難完整的了解到底發(fā)生了什么以及什么時候發(fā)生的。 這個時候就輪到 console.trace(在終端的話就只有 trace )出場來調(diào)試 JavaScript了 。
假設(shè)現(xiàn)在想看 car 實(shí)例在第33行調(diào)用 funcZ 函數(shù)的完整堆棧軌跡信息:
var car;
var func1=function() {
func2();
}
var func2=function() {
func4();
}
var func3=function() {
}
var func4=function() {
car=new Car();
car.funcX();
}
var Car=function() {
this.brand=‘volvo’;
this.color=‘red’;
this.funcX=function() {
this.funcY();
}
this.funcY=function() {
this.funcZ();
}
this.funcZ=function() {
console.trace(‘trace car’)
}
}
func1();
var car;
var func1=function() {
func2();
}
var func2=function() {
func4();
}
var func3=function() {
}
var func4=function() {
car=new Car();
car.funcX();
}
var Car=function() {
this.brand=‘volvo’;
this.color=‘red’;
this.funcX=function() {
this.funcY();
}
this.funcY=function() {
this.funcZ();
}
this.funcZ=function() {
console.trace(‘trace car’)
}
}
func1();
第33行會輸出:
在這里我們可以看見到func1調(diào)用了func2, func2又調(diào)用了func4。Func4 創(chuàng)建了Car的實(shí)例,然后調(diào)用了方法car.funcX,等等。
盡管我們感覺我們已經(jīng)對自己的腳本代碼非常了解,但是這種分析依然有用。比如優(yōu)化代碼。獲取到堆棧軌跡信息和一個所有相關(guān)函數(shù)的列表。每一行都是可點(diǎn)擊的,并且可以在他們中間前后穿梭。這種感覺非常的棒。
有時候你發(fā)現(xiàn)產(chǎn)品有一個問題,而 source map 并沒有部署到服務(wù)器。不要害怕。Chrome 可以格式化 JavaScript 文件,使之易讀。格式化出來的代碼在可讀性上可能不如源代碼 —— 但至少你可以觀察到發(fā)生的錯誤。點(diǎn)擊源代碼查看器下面的美化代碼按鈕 {} 即可。
我們來看看怎么在函數(shù)中設(shè)置斷點(diǎn)。
通常情況下有兩種方法:
這兩種方法都必須在文件中找到需要調(diào)試的那一行。
使用控制臺是不太常見的方法。在控制臺中使用 debug(funcName),代碼會在停止在進(jìn)入這里指定的函數(shù)時。
這個操作很快,但它不能用于局部函數(shù)或匿名函數(shù)。不過如果不是這兩種情況下,這可能是調(diào)試函數(shù)最快的方法。(注意:這里并不是在調(diào)用 console.debug 函數(shù))。
var func1=function() {
func2();
};
var Car=function() {
this.funcX=function() {
this.funcY();
}
this.funcY=function() {
this.funcZ();
}
}
var car=new Car();
在控制臺中輸入 debug(car.funcY),腳本會在調(diào)試模式下,進(jìn)入 car.funcY 的時候停止運(yùn)行:
如今,經(jīng)常在應(yīng)用中引入多個庫或框架。其中大多數(shù)都經(jīng)過良好的測試且相對沒有缺陷。但是,調(diào)試器仍然會進(jìn)入與此調(diào)試任務(wù)無關(guān)的文件。解決方案是將不需要調(diào)試的腳本屏蔽掉。當(dāng)然這也可以包括你自己的腳本。 點(diǎn)此閱讀更多關(guān)于調(diào)試不相關(guān)代碼(http://raygun.com/blog/javascript-debugging-with-black-box/)。
在更復(fù)雜的調(diào)試中,我們有時需要輸出很多行。你可以做的事情就是保持良好的輸出結(jié)構(gòu),使用更多控制臺函數(shù),例如 console.log,console.debug,console.warn,console.info,console.error 等等。然后,你可以在控制臺中快速瀏覽。但有時候,某些JavaScrip調(diào)試信息并不是你需要的。
現(xiàn)在,可以自己美化調(diào)試信息了。在調(diào)試JavaScript時,可以使用CSS并自定義控制臺信息:
console.todo=function(msg) {
console.log(‘ % c % s % s % s‘, ‘color: yellow; background - color: black;’, ‘–‘, msg, ‘–‘);
}
console.important=function(msg) {
console.log(‘ % c % s % s % s’, ‘color: brown; font - weight: bold; text - decoration: underline;’, ‘–‘, msg, ‘–‘);
}
console.todo(“This is something that’ s need to be fixed”);
console.important(‘This is an important message’);
輸出:
例如:
在console.log()中, 可以用%s設(shè)置字符串,%i設(shè)置數(shù)字,%c設(shè)置自定義樣式等等,還有很多更好的console.log()使用方法。 如果使用的是單頁應(yīng)用框架,可以為視圖(view)消息創(chuàng)建一個樣式,為模型(models),集合(collections),控制器(controllers)等創(chuàng)建另一個樣式。也許還可以像 wlog,clog 和 mlog 一樣發(fā)揮小伙伴們的想象力!
在 Chrome 瀏覽器的控制臺(Console)中,大家的注意力都集中在具體的函數(shù)上。每次這個函數(shù)被調(diào)用,它的值就會被記錄下來。
var func1=function(x, y, z) {
//....
};
然后輸出:
這是查看將哪些參數(shù)傳遞到函數(shù)的一種很好的方法。但在這里小編要說明,如果控制臺能夠告訴我們需要多少參數(shù),那就好了。在上面的例子中,函數(shù)1期望3個參數(shù),但是只有2個參數(shù)被傳入。如果代碼沒有在代碼中處理,它可能會導(dǎo)致一個 bug 。
在控制臺中執(zhí)行 querySelector 一種更快的方法是使用美元符。$(‘css-selector’) 將會返回第一個匹配的 CSS 選擇器。$$(‘css-selector’) 將會返回所有。如果你使用一個元素超過一次,它就值得被作為一個變量。
很多開發(fā)人員都使用 Postman 來處理 Ajax 請求。Postman 真不錯,但每次都需要打開新的瀏覽器窗口,新寫一個請求對象來測試。這確實(shí)有點(diǎn)兒煩人。
有時候直接使用在用的瀏覽器會更容易。
這樣的話,如果你想請求一個通過密碼保證安全的頁面時,就不再需要擔(dān)心驗(yàn)證 Cookie 的問題。這就是 Firefox 中編輯并重新發(fā)送請求的方式。
打開探查器并進(jìn)入網(wǎng)絡(luò)頁面,右鍵單擊要處理的請求,選擇編輯并重新發(fā)送。現(xiàn)在你想怎么改就怎么改。可以修改頭信息,也可以編輯參數(shù),然后點(diǎn)擊重新發(fā)送即可。
現(xiàn)在我發(fā)送了兩次同一個請求,但使用了不同的參數(shù):
DOM 是個有趣的東西。有時候它發(fā)生了變化,但你卻并不知道為什么會這樣。不過,如果你需要調(diào)試 JavaScript,Chrome 可以在 DOM 元素發(fā)生變化的時候暫停處理。你甚至可以監(jiān)控它的屬性。在 Chrome 探查器上,右鍵點(diǎn)擊某個元素,并選擇中斷(Break on)選項(xiàng)來使用:
為了幫助小伙伴們更好的學(xué)習(xí)Python,技術(shù)學(xué)派整理了Python的相關(guān)學(xué)習(xí)視頻及學(xué)習(xí)路線圖。
關(guān)注“技術(shù)學(xué)派”后,評論轉(zhuǎn)發(fā)文章,私信回復(fù):Python學(xué)習(xí)
*請認(rèn)真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。