整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          JavaScript基礎知識22-斷點調試

          嘍,你好啊,我是雷工!

          斷點調試是程序猿必備的調錯,梳理邏輯的技能;當遇到程序報錯,或者程序邏輯理解不了,都可以通過斷點調試來輔助解決遇到的問題。

          斷點調試是程序猿必不可少的技能,本節學習斷點調試,以下為學習筆記。

          1、斷點調試

          ● 作用:學習時可以幫助更好地理解代碼運行,工作時可以更快找到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)是面向字節系小程序開發者推出的桌面端集成開發環境,支持小程序開發、調試、預覽、上傳等基本功能,旨在幫助開發者更高效地開發小程序,我也是負責本地開發能力的建設。

          因為工作原因最近對斷點調試進行一些研究,百度了一下,遺憾的是發現網絡上大部分內容都是在教學如何使用調試工具,并沒有擴展到具體的細節,譬如通信邏輯,基本原理等。因此,為了嘗試去弄懂一些斷點調試的底層邏輯,特意去找了一些英文文檔并實踐。

          前言

          作為一個前端開發,前端調試的方式一般有如下幾種:

          1. 代碼中直接打印,比如很多時候直接在代碼中使用 console 來打印一些變量,或者在 vscode 中使用 Turbo Console Log 等插件生成特色的日志內容。
          2. debugger,在代碼中輸入 debugger 關鍵字,然后在瀏覽器中進行斷點調試,或者在瀏覽器中找到源碼,然后進行斷點調試。
          3. vscode 自帶的 web 調試能力。

          相比于 console,debugger 可以看到代碼實際的執行路線以及每個變量的變化,代碼可以跳著看,也可以針對某個函數步步執行。

          但是 console 與 debugger 方式對代碼都有侵入,在開發階段可能要不斷增加和移除來調試,如果不小心忘了,那 mr 又得打回并重新提交了…

          相信很多人在提 mr 都有類似經驗…

          相對來說,瀏覽器中找到 source 源碼打斷點是一個更好的方式,但是還是需要打開 Devtools ,并在 sources 面板找到文件注入斷點,操作上也是有點小麻煩。

          因此第 3 種方式,可能是不錯的方式,在 vscode 中直接在源碼中調試,并能看到具體的變量信息和網頁效果。

          實際上,瀏覽器打斷點與在 vscode 打斷點本質原理都類似。下面就聊一聊瀏覽器斷點調試和 vscode 斷點調試的原理。

          基本知識

          Chrome Devtools Protocol

          在了解具體場景之前,首先有一個比較重要的概念,那就是 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 信息包括:

          • DOM:提供了對文檔對象模型的訪問和操作接口,如節點遍歷、樣式計算、事件處理等。
          • CSS:提供了對樣式表的訪問和操作接口,如樣式計算、應用、修改等。
          • Network:提供了對網絡請求和響應的訪問和操作接口,如請求攔截、修改、模擬等。
          • Console:提供了對瀏覽器控制臺的訪問和操作接口,如日志記錄、錯誤捕獲、命令執行等。
          • Debugger:提供了對瀏覽器調試器的訪問和操作接口,如斷點設置、單步執行、變量查看等。
          • Performance:提供了對瀏覽器性能分析的訪問和操作接口,如性能指標獲取、性能分析報告生成等。

          這幾個也是平常開發中最常用到的幾個 Domains 了。

          常見 CDP

          • Page:Page.navigate:頁面跳轉
          • Network:Network.enable:開啟網絡,可以用來模擬網絡開閉能力
          • DOM:DOM.getDocument:獲取頁面樹,比如調試器 Elements 面板的展示
          • CSS:CSS.getComputedStyleForNode:返回給定 nodeId 的所有樣式,比如點擊 dom 節點,展示 css
          • Runtime:Runtime.evaluate:在當前頁面中執行 JavaScript 代碼
          • Debugger:下面會提到很多,譬如 Debugger.pause/Debugger.setXXX

          應用場景

          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 之間通信:

          • 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 表示告訴頁面要設置一個調試斷點了;setBreakpointByUrl 則是告訴頁面設置的具體信息;getPossibleBreakpoints 表示設置以后獲取正確的斷點位置,并展示藍色小塊。

          有時候可能會發現設置了某一行為斷點,但是斷點的位置并不是指向的位置,而是另外的位置。比如上面截圖,如果在 15 行設置斷點,則最后展示斷點位置為 18 行。

          整體流程如下圖:

          移除斷點

          除了在 sources 面板增加斷點,還可以取消斷點。取消斷點的 CDP 非常簡單, Devtools 會給 Web Page 發送一個 Debugger.removeBreakpoint 來移除斷點。

          實時斷點調試

          當點擊完斷點以后,頁面會走到斷點所在的代碼位置,同時 Devtools 會接收到一些 CDP 消息,通知它當前斷點的狀態和上下文信息。

          我寫了一個實例,是關于數字的增減邏輯,并在數字增加的時候,走到斷點位置(不需要刷新頁面)。

          可以看到,當點擊 + 號以后,頁面就進入斷點調試邏輯,此時 Devtools 會收到 Debugger.paused消息:

          • Debugger.paused:Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria.

          此時表示頁面已經暫停了代碼執行,Devtools 可以通過 Debugger.paused事件中的參數,獲取當前斷點的上下文信息,如斷點所在的函數、變量值、堆棧信息等。

          具體信息沒有對應看

          點擊“Step Over next function call”(按鈕 1),Devtools 會收到 Debugger.resumed r??zu?m d 消息,通知繼續執行代碼。

          • Debugger.resumed:Fired when the virtual machine resumed execution.

          隨后代碼跳到下一行,此時又會收到 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.resumedDebugger.paused消息,進入到函數內部。

          • Debugger.stepInto:Steps into the function call.

          點擊“Step out of current function”,Devtools 會發送 Debugger.stepOut消息,并收到 Debugger.resumedDebugger.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();
            }
          

          很清晰的看到,上面提到的各種操作邏輯的函數,譬如 pausestepXXX等 API。

          這里列舉幾個操作按鈕通信較多的 API。

          pause() 的主要邏輯為 2 點:

          1. 設置使頁面斷點暫停狀態為 ture。
          2. 發送 Debugger.paused消息到頁面。

          stepInto() 的主要邏輯為:

          1. 拿到跳轉的 skipList,它是一個字符串數組,用于指定要跳過的函數名稱列。在操作調試按鈕時,一般都是空數組。
          2. 發送 Debugger.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_agentauction_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.evaluateOnCallFrameRuntime.getProperties 可以拿到一些環境信息,前者比如一些 number 數字就可以得到。

          Vscode Web 代碼斷點調試原理

          在 Vscode 中調試代碼,能讓開發者專注于代碼本身,一邊開發運行一邊斷點調試查看變量信息,并減少一些臟代碼的開發。如下圖所示,可以看到,似乎是將瀏覽器的 Debugger 的邏輯照搬到了 Vscode 中。

          在介紹完瀏覽器斷點調試的邏輯以后,我們大概了解了頁面與 Devtools 的通信過程和相關 CDP 信息。有了這些基礎,我們再分析分析 Vscode 中是如何實現斷點調試 Web 代碼的。

          launch.json

          在 Vscode 中配置調試后,會生成一個 .vscode/launch.json 文件,其主要是配置需要調試的 url 和遠程調試的端口號 port。

          {
            "version": "0.2.0",
            "configurations": [
              {
                "type": "chrome",
                "request": "launch",
                "name": "針對 localhost 啟動 Chrome",
                "url": "http://localhost:8080",
                "webRoot": "${workspaceFolder}"
              }
            ]
          }
          

          Debugging Architecture of VS Code

          [?ɑrk??tekt??r]

          Vscode 并不只是前端開發者調試 JS 使用,還可以調試其他語言,Python 一些教程就建議使用 Vscode 調試。因此 Vscode 的調試架構高度靈活,可以支持多種編程語言和調試場景,并且可以基于該架構實現各種調試擴展。

          如上圖,Vscode 的調試架構中,有 3 個 Core Module:

          • Debug Adapter:調試適配器是 Vscode 和具體調試目標之間的橋梁。適配器主要就是負責將調試請求轉換為調試目標,并將調試目標轉為調試器需要的結果,其通過 Vscode Debug Protocol 協議通信。Debug Adapter 提供了一組標準的調試接口,包括設置斷點、單步執行、查看變量值等。
          • Debug Extension:調試擴展是 Vscode 內置的插件,提供特定語言或者場景的實現。比如可以調試 JS TS Python 等,同時社區也可以提供相關擴展,譬如 Java:
          • Debug UI:即 Vscode 的操作界面,它提供了調試器的各種操作和功能,例如設置斷點、單步調試、查看變量值等。

          :別忘了另外一個 Debugger,即為 launch.json 中的 type,指底層的調試目標,例如 Node.js 運行時、Chrome 瀏覽器等等。比如斷點后的信息需要傳遞給 chrome,需要去暫定代碼執行,并斷點逐步執行等。

          原理

          在了解原理之前,先看一些現象:

          1. 當 Vscode 啟動調試并走到指定斷點時,Chrome 自身調試器也會走到對應的調試邏輯(Devtools 本身也是一個 ws client,任何 client 都會收到 chrome 的 cdp 消息)。
          2. 當在 Vscode 調試面板操作 StepInto 按鈕時,Vscode 代碼會走到下一步,同時 chrome 調試也會走到下一步。
          3. 當在 Chrome Devtools 中操作 StepInto 按鈕時,Chrome Devtools 代碼也會走到下一步,同時 Vscode 中代碼也會跳轉到下一步。

          通過上面 3 種現象可以看出,Vscode Webpage Devtools 關系如下:

          細品一下,這時候就可以知道為何需要 Debug Adapter 了。實際上,就是將 CDP 消息轉為 DAP。

          Workflow

          Vscode Chrome Debug 的工作流程如下:

          1. Vscode 啟動 JavaScript 調試器,并配置調試器相關的參數,例如調試類型、調試目標等等。
          2. JavaScript Debug Extension 會根據配置啟動一個 Debug Adapter 進程,并向該進程發送調試請求,請求 Debug Adapter 與 Debugger 之間建立連接。
          3. Debug Adapter 進程會根據用戶的配置,啟動相應的 Chrome,并與對應網頁(Debugger)建立連接。
          4. Debug Adapter 進程會將調試結果轉換為 Vscode 支持的調試消息格式,即上面提到的 DAP,并將調試消息發送給 Vscode。

          這里的核心就是 Extension,其作用就是調度與控制,比如啟動 Adapter 進程,發送與接收調試信息等等,屬于大 BOSS,而 Adapter 只是下屬。

          JS Debug Extension

          上面提到,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 相關邏輯。

          Command 實例

          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 加載

          SourceMap 雖然也是靜態資源,但是其加載在 Network 面板并不能看到,而是在 Developer Resources 中。

          為了啟動快,我用的 Vite 來生成項目。Vite 利用了瀏覽器原生的 ES modules 功能,根據文件依賴關系,生成依賴樹,然后各模塊文件模塊單獨加載。Vite 文件都有單獨的 SourceMap,不需要配 SourceMap 依賴。

          可以看到,這里 Vite 默認是直接內嵌的 SourceMap,無需單獨請求, 可以在代碼文件加載完成后,就直接解析了,紅框里面展示的鏈接就是 Base64 的形式了。

          ??SourceMap 的解析是交給 Devtools 本身的,Debugger 只負責運行和暫停。因此,如果斷點在 SourceMap 解析完成之前觸發,則沒法告訴 Debugger 正確的地址,可能會出現斷點無效情況。

          IDE 小程序斷點調試

          Devtools Debug

          根據上面的介紹,小程序斷點調試的最簡單辦法就是在代碼中寫上 debugger,然后交給 v8 處理即可。另外還有一種方式就是打開小程序調試器,在 sources panel 中打斷點,如下圖:

          打斷點,刷新小程序,即可跳轉到斷點位置。此時可以看到對應的 CDP 消息中的 Request。

          可以看到,這里點擊的是 56 行,但實際上 Request 中卻不是,Devtools 通過 sourceMap 進行了處理,定位到了 64 行。根據上面提到的源碼調試邏輯,這里的位置為編譯后的代碼位置,找到編譯產物代碼 app.js 即可看到 real position。

          IDE Editor Debug???

          考慮到上面提到的 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

          為什么要使用 debugger

          這篇文章將介紹如何使用斷點來進行 JavaScript 調試。在讀這篇文章之前,需要問一個問題:為什么要使用斷點來進行調試?

          我們首先需要認可使用斷點的是必要的,否則下文介紹的所有斷點調試方法都會是廢話。console.log 是前端開發最常用的調試手段,它簡單直接解決一部分問題。但當遇到十分復雜的問題,console.log 就會變得不趁手。比如:

          • 一個邏輯復雜的算法

          如果你刷過 leetcode 一定深有體會,算法某個測試用例報錯了,有時很難光靠目測找出有問題的那個方法。

          • 一個復現步驟十分繁瑣的bug。

          花了10分鐘好不容易復現了,但是只跟蹤到某行代碼,需要第二次添加 log 才能繼續尋找問題。查看log -> 添加log -> 查看log... 這個過程重復幾遍,今天剩下的磚就搬不完了。

          • 一段運行流程冗長的代碼
          • 一段沒有注釋、起名隨意的代碼
          • server 端代碼

          有 nodejs 服務端開發經驗的同學相信有過在 postman 和 ide 之間反復橫跳的經歷,如果光靠 log,對于一個巨大的復雜對象,控制臺是不好查看全貌的。如果一個接口還涉及到數據庫增刪、第三方依賴,那么復原上一次請求造成的后果也是一件痛苦的事情。

          在這些情況下,斷點調試是非常有價值的,將 debug 的時間復雜度從 O(n) 降到 O(1),讓搬磚更快樂。

          這是文章的內容大綱:

          • Chrome debugger 基本用法
          • VS Code 調試 SPA 應用
          • Chrome 調試 Nodejs
          • VS Code 調試 Nodejs


          Chrome debugger 基本用法

          最簡單的斷點調試,就是在代碼中加一句 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 來簡化打斷點的方式。


          VS Code 調試 SPA 應用

          首先我們使用 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 字段要根據項目配置進行修改。


          Chrome 調試 Nodejs

          上文講的是如何調試頁面,接下來我們聊如何調試 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 就可以進入調試了。

          使用 VS Code 調試 Nodejs

          到此為止,我們已經達成調試 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,然后正常啟動調試就行。


          Conditional Breakpoint 條件斷點

          在某些情況下,我們不希望打上的每個斷點都發揮作用,而是在執行到斷點那行,且滿足某個條件再中斷代碼執行。這就是條件斷點。

          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 按鈕,點擊就可以選擇自己需要添加的調試配置。


          主站蜘蛛池模板: 手机看片一区二区| 亚洲av福利无码无一区二区| 亚洲一区二区三区国产精品| 精品视频一区二区三区四区 | 一区二区三区杨幂在线观看| 精品视频一区二区三区在线播放| 国产精品一区电影| AV天堂午夜精品一区| 国产成人一区二区三区免费视频| 亚洲天堂一区在线| 美女AV一区二区三区| 成人免费视频一区二区| 日本伊人精品一区二区三区| 精品亚洲av无码一区二区柚蜜| 一区二区三区日本电影| 日韩一区二区三区无码影院 | 日韩一本之道一区中文字幕| 中文字幕精品亚洲无线码一区| 亚洲一区二区三区高清视频| 国产aⅴ精品一区二区三区久久| 精品aⅴ一区二区三区| 精品国产免费一区二区| 91精品一区二区三区久久久久 | 久久久综合亚洲色一区二区三区| 中文字幕亚洲综合精品一区| 精品国产亚洲一区二区三区在线观看 | 久久国产免费一区| 一区二区三区国模大胆| 国产精品揄拍一区二区| 亚洲AV综合色一区二区三区| 天码av无码一区二区三区四区| 国精产品一区一区三区有限公司 | 在线精品视频一区二区| 天天视频一区二区三区| 国产精品久久久久一区二区 | 一区二区三区日韩| 麻豆va一区二区三区久久浪| 成人国产精品一区二区网站| 亚洲A∨无码一区二区三区| 亚洲欧美日韩中文字幕在线一区 | 日本一区二三区好的精华液 |