<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深入前端調(diào)試原理

          共 10104字,需瀏覽 21分鐘

           ·

          2023-09-01 03:42

          調(diào)試是開發(fā)者需要掌握的一項重要的技能, 它能夠幫助我們快速定位和修復(fù)代碼中的問題。本文主要介紹前端調(diào)試的基本原理。

          本文是筆者在學(xué)習(xí)了 前端調(diào)試通關(guān)秘籍[1] 后,并結(jié)合平時實踐過程中一些經(jīng)驗進(jìn)行的梳理和總結(jié)。主要以 Chrome,VSCode 作為調(diào)試工具,在其他編輯器中,配置雖有不同,但原理是相通的。

          本文所使用的示例代碼均在 debug-dojo[2] 倉庫。

          從一個簡單的示例入手

          以上面代碼為例,簡單實現(xiàn)了按鈕在點擊后文字更新點擊次數(shù)的能力:

          當(dāng)我們打開 Devtools 中的 Sources 目錄,并在 click 的回調(diào)設(shè)置斷點后,再次點擊按鈕,程序就會在此停住,并將內(nèi)部的運(yùn)行狀態(tài)暴露出來:

          這背后到底是怎么實現(xiàn)的呢?Devtools 是如何將程序內(nèi)部的運(yùn)行狀態(tài)暴露出來,并且通過 UI 的形式呈現(xiàn)的呢?

          Chrome Devtools 原理

          Devtools 主要包含以下四個關(guān)鍵的組成部分:

          • 后端:和 Js Runtime,內(nèi)部的布局、渲染器深度綁定,用于將內(nèi)部的狀態(tài)通過協(xié)議暴露出來。
          • 前端:這里主要是 Devtools 的各個調(diào)試模塊,負(fù)責(zé)對接協(xié)議,做 UI 展示和交互。前端本質(zhì)上是獨(dú)立的,任何對接協(xié)議用于展示數(shù)據(jù)的項目都可以作為調(diào)試前端。
          • 通信方式:前端后端通過 websocket 進(jìn)行通信。
          • Chrome Devtools Protocol(簡稱:CDP): 前后端的通信協(xié)議。

          CDP 協(xié)議的具體內(nèi)容可以通過 官網(wǎng)文檔[3] 進(jìn)行查看,它按照不同的域進(jìn)行劃分,基本上包含我們平時所使用的 Devtools 的不同場景:

          可以在 Chrome Devtools 設(shè)置中打開 Protocol Monitor,就可以查看前后端的協(xié)議通信了:

          VSCode Debugger 原理

          除了 Devtools 外,VSCode Debugger 也是常見的調(diào)試工具,在 VSCode 的項目中 .vscode/launch.json 中加入如下的配置即可調(diào)試:

          VSCode Debugger 的原理大致相同,唯一特殊的是:VSCode 并不是 JS 語言的專屬編輯器,它可以用于多種語言的開發(fā),自然不能對某一種語言的調(diào)試協(xié)議進(jìn)行深度適配,所以它提供了 Debugger 適配層和適配器協(xié)議,不同的語言可以通過提供 Debugger 插件的形式,增加 VSCode 對不同語言的調(diào)試能力:

          如此,VSCode Debugger 就能以唯一的 Debugger 前端調(diào)試各個不同的語言,插件市場中也提供了諸多不同語言的調(diào)試插件:

          調(diào)試模式

          Attach 模式

          從上面調(diào)試的四個關(guān)鍵部分可以看出,前后端之間是通過 websocket 進(jìn)行通信的,所以確保前后端能正確的連接是調(diào)試成功的關(guān)鍵。

          除了在需要調(diào)試的網(wǎng)頁中直接打開 Devtools 的方式外,我們使用第三方前端工具進(jìn)行調(diào)試時,都需要知道所需要調(diào)試的網(wǎng)頁的后端 ws 地址才行。因此我們需要讓 Chrome 以指定調(diào)試端口的形式跑起來,使用 --remote-debugging-port 參數(shù)指定調(diào)試端口:

          /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222

          Chrome 運(yùn)行參數(shù)非常多,可以通過 該文檔[4] 進(jìn)行查看。

          在 Chrome 運(yùn)行起來后,隨意的打開幾個網(wǎng)頁,然后訪問 localhost:9222/json/list 網(wǎng)址,此時就能得到所有運(yùn)行的網(wǎng)頁后端 ws 地址:

          在百度網(wǎng)頁中同時打開了 Devtools, 可以看到連 Devtools 的調(diào)試信息都一起打印出來了,因為 Devtools 本質(zhì)上也是一個網(wǎng)頁程序,所以甚至可以做到對 Devtools Debugger 進(jìn)行 Debug 這樣的套娃操作。

          有了 ws 信息,我們就可以使用其他 Debugger 進(jìn)行連接了,比如使用下面的 VSCode Debug 配置:

          Node.js 程序在運(yùn)行時則是通過 --inspect 或者 --inspect-brk 參數(shù)開啟調(diào)試模式:

          Node.js 調(diào)試協(xié)議經(jīng)過了漫長的迭代,最終也是以 CDP 協(xié)議進(jìn)行調(diào)試,因此 Node.js 程序可以直接使用 Devtools 進(jìn)行調(diào)試,在 Chrome 中訪問 chrome://inspect/#devices 就可以看到調(diào)試目標(biāo)了:

          如果當(dāng)前的 Node 項目沒有被發(fā)現(xiàn),可能是檢測端口不是默認(rèn)的 9229 ,可以通過 Configure 進(jìn)行配置。

          VSCode Debugger 同樣能夠連接上 Node.js 項目并進(jìn)行調(diào)試,

          Launch 模式

          前面講到的都是首先通過調(diào)試模式啟動一個項目,然后再手動進(jìn)行前端 ws 連接,最后再調(diào)試的模式。相對較繁瑣,VSCode Debugger 提供了 launch 模式,它相當(dāng)于是將上面的流程自動化了,以調(diào)試模式將 Chrome 或者 Node.js 程序運(yùn)行起來,然后 Debugger 自動 attach 到調(diào)試端口,然后直接調(diào)試。

          VSCode Debugger 的各種調(diào)試配置不是本文的重點,有興趣可以閱讀 官方文檔[5],已經(jīng)講得很詳細(xì)了,也提供常見使用場景的 Recipes[6]。

          SourceMap

          實際的項目往往不會如此簡單,要引入各種開源庫,然后經(jīng)過諸如 Webpack, Rollup 等等打包工具做編譯打包,才能運(yùn)行起來。編譯壓縮后的代碼是不具備可讀性的,在它上面進(jìn)行調(diào)試也沒有意義,所以我們需要一個技術(shù),將源碼和編譯后的代碼進(jìn)行映射,這就是 SourceMap。

          以如下代碼為例:

          SourceMap 文件規(guī)則

          在經(jīng)過 Webpack 打包后,會生成壓縮文件,以及對應(yīng)的 SourceMap 文件:

          SourceMap 文件內(nèi)容主要包含:

          • version: SourceaMap 的版本
          • file: 對應(yīng)編譯后的文件名
          • sourceRoot:源碼的根目錄
          • sources:對應(yīng)源碼文件路徑
          • sourcesContent:對應(yīng)的源碼文件內(nèi)容
          • names:源碼轉(zhuǎn)化前的變量,屬性名
          • mappings:源碼和編譯后代碼的對應(yīng)關(guān)系

          mappings 使用了 BaseVLQ 編碼形式

          • 使用 ,; 做分隔,一個 ; 對應(yīng)轉(zhuǎn)換后代碼的一行,, 代表一個位置的映射
          • 每個 maping 通常使用 5 位長度表示映射關(guān)系(也會根據(jù)實際的打包規(guī)則進(jìn)行簡化),1 到 5 位分別表示:
            • 對應(yīng) 轉(zhuǎn)換后 代碼第幾列(行號已經(jīng)通過 ; 確定)
            • 對應(yīng)轉(zhuǎn)換前哪個文件(對應(yīng) sources 里面下標(biāo)索引)
            • 對應(yīng)轉(zhuǎn)換前第幾行
            • 對應(yīng)轉(zhuǎn)換前第幾列
            • 對應(yīng)轉(zhuǎn)換前源碼哪個變量名(對應(yīng) names 里面的下標(biāo)索引)

          說起來有點繞,好在可以通過 在線的 SouceMap 可視化工具[7] 進(jìn)行查看:

          SourceMap 提供的是源碼和編譯后代碼的映射能力,無關(guān)乎代碼的類型,所以在不管是 js 代碼,還是 less 代碼,都可以為其提供映射:

          Webpack SourceMap 配置

          Webpack 提供的 SourceMap 配置能力應(yīng)該是最豐富,也是最復(fù)雜的,基本上掌握了 Webpack 的 SourceMap 配置,其他的打包工具就難不倒我們了。

          在配置之前,首先說明幾個概念:

          • Original:源碼
          • Transformed:經(jīng)過各種 loader 轉(zhuǎn)化后的代碼,比如 babel-loader, less-loader 等等
          • Genrated: weback 對每一個模塊按照 Webpack 加載處理后的代碼
          • Bundled: 最中生成的代碼

          devtool 配置參見 官方文檔[8],需要滿足 [inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map 這樣的規(guī)則。

          inline|hidden|eval 控制 SourceMap 文件的生成方式,當(dāng)不指定時,會單獨(dú)生成 SourceMap 文件,并在對應(yīng)的打包文件的尾部添加關(guān)聯(lián):

          hidden 則表示單獨(dú)生成 SourceMap 但是不關(guān)聯(lián)。

          inline 則表示將生成的 SourceMap 內(nèi)容以 Base64 形式直接內(nèi)嵌到打包文件中,打包的文件體積會顯著增加:

          eval 相對而言比較復(fù)雜,JS 可以通過 eval api 動態(tài)執(zhí)行代碼:

          我們可以通過后面映射的 VM242:1 虛擬文件查看源碼,設(shè)置斷點,注意:該文件不會出現(xiàn)在 Sources 面板的目錄中。

          如果在 eval 代碼的尾部加上 //#sourceURL=xxx,那么 Devtools 就會以 xxx 的路徑將文件加入到 Sources 面板目錄中,就好像是直接運(yùn)行 xxx 路徑的文件代碼一樣:

          Webpack 利用 eval 來優(yōu)化 SourceMap 生成的性能,是比較推薦的 development 模式下配置,它將每個模塊都用 eval 包裹,并搭配 sourceURL, 就能將也映射到每個源文件了,不需要從 Bundled 的代碼做映射。但現(xiàn)在映射到的只是 Generated 級別的代碼:

          所以只是 sourceURL 還是不夠的,需要搭配其他 SourceMap 配置,比如 eval-source-map,就能進(jìn)一步將 SourceMap 關(guān)聯(lián)上:

          此時的行為就和 inline 模式的差不多。

          nosources 表示不將對應(yīng)源碼寫入 SourceMap 的 sourcesContent 字段中,這會顯著的減少 SourceMap 文件的體積。

          cheap 表示映射只精確到行,而不到列,也可以有效的加快 SourceMap 的生成速度,畢竟精確到行已經(jīng)足夠我們排查問題了。

          module 主要用來處理 loader 生成的 SourceMap。通常代碼要經(jīng)過多個 loader 處理轉(zhuǎn)換,比如 React 組件代碼必須得經(jīng)過 babel-loader 進(jìn)行轉(zhuǎn)換,然后在交由 Webpack 處理,而在 loader 的轉(zhuǎn)換過程也會生成 SourceMap,如果不指定 module,Webpack 只能生成從 Transformed 代碼到 Bundled 代碼的映射,即映射級別只能到 Transformed。在指定了 module 后,Webpack 會結(jié)合 loader 過程中生成的 SourceMap, 和自己從 Transformed 代碼到 Bundled 代碼的映射,就能生成 Original 級別的映射了。

          需要注意的是:即使開啟了 module,Webpack 也只會處理經(jīng)由 loader 傳遞下來的 SourceMap,我們常常會在 babel-loader 中排除 node_modules 里面的文件處理,因為大多數(shù)情況下里面的開源包都是轉(zhuǎn)換后的產(chǎn)物,直接交由 Webpack 處理即可,但如果庫里面也包含 SourceMap,Webpack 是不會處理它的,所以此時只能映射到開源庫的打包產(chǎn)物級別。

          @antv/s2 為例:

          即使開啟了 module,也只能映射到 esm/index.js 這個級別:

          為了能正確加上開源庫的 SourceMap 信息,需要搭配 source-map-loader[9] 處理開源庫的 SourceMap, 然后傳遞給 Webpack,Webpack 就能正確的處理經(jīng)由 loader 傳遞下來的 SourceMap 了,配置如下:

          現(xiàn)在再來看官方文檔中不同配置之間的速度差異,以及 Production 級別,是不是就能清楚一點了。

          SourceMap 加載

          編譯后的代碼以及 SourceMap 都有了,現(xiàn)在來講講 SourceMap 是如何被 Devtools 加載解析的。

          瀏覽器雖會加載 SourceMap 資源,但它們并不會出現(xiàn)在 Network 面板中,需要在 Developer Resources 面板中查看:

          不過目前 Developer Resources 面板比較的簡單,只能查看成功與否等簡單信息。

          以單獨(dú)的 SourceMap 文件為例,當(dāng) Devtools 加載完代碼文件后,如果文件尾部包含 //#sourceMappingURL ,就會單獨(dú)去請求該鏈接,在拿到 SourceMap 文件后再開始做解析,壞處就是多了一個的網(wǎng)絡(luò)請求,解析速度就會慢一點。

          inline 模式則是直接內(nèi)嵌的 SourceMap,無需單獨(dú)請求, 可以在代碼文件加載完成后,就直接解析了。inline 模式下 Developer Resources 里面展示的鏈接就是 Base64 的形式了:

          上面說過 hidden 完全不關(guān)聯(lián) SourceMap, 所以 Devtools 是不會去加載 SourceMap 的。這種模式主要用于生產(chǎn)環(huán)境,我們并不希望直接在線上暴露源碼,所以不能直接關(guān)聯(lián)上。而是將生成的 SourceMap 上傳到監(jiān)控平臺,就可以結(jié)合線上的報錯信息順利找到報錯的源碼位置了。

          當(dāng)然還可以借助 Sources 面板中的 Add source map... 臨時添加源碼映射,不過重新刷新后就沒了:

          nosources 不將源碼寫入 SourceMap 文件中,既能減少 SourceMap 文件體積,也能到達(dá)隱藏源碼信息,比如我們在源碼中打印了些信息出來,雖然從 Console 面板中看到映射到了正確的文件路徑,但是點擊跳轉(zhuǎn)過去后,會發(fā)現(xiàn)無法查看源碼:

          這種模式下,借助 VSCode Debugger 又有別樣的體驗。編輯器調(diào)試的好處在于:如果映射出來文件在當(dāng)前的工程項目中,編輯器就會直接打開該文件,不管有沒有源碼存在于 SourceMap 中。調(diào)試配置如下:

          如果映射的文件路徑不在當(dāng)前項目中,那么打開的結(jié)果就和 Devtoos 一樣了。

          多說一句,將源碼加入到 SourceMap 中后。如果映射到的文件在當(dāng)前項目中,那么跳轉(zhuǎn)過去后,是可以直接進(jìn)行編輯的;如果不在,則該文件就只讀。比如下面的 @antv/s2 源碼中的某一個文件顯然不在項目中,雖然可以查看源碼,但是無法編輯:

          eval 模式的加載情況情況大致相同,只是多了將 //#sourceURL 映射到 Sources 目錄的布置而已,sourceMappingURL 處理就和 inline 模式一致。所以不再贅述。

          SourceMap 加載到底發(fā)生在哪里?

          SourceMap 的加載和解析完全是前端行為(Devtools,VSCode Debugger)等,后端并不涉及到任何 SourceMap 的處理。比如我們在源碼位置添加的斷點,通過 Protocol Monitor 可以看到傳給后端的是打包產(chǎn)物代碼的位置:

          其實也能理解這樣的設(shè)計,本來源碼的調(diào)試和斷點就是前端行為,后端只是提供了運(yùn)行時暫停和狀態(tài)暴露的能力。如果后端來解析,會影響代碼運(yùn)行時的執(zhí)行效率。而且前端處理能更自由,不同 Debugger 工具可能對 SourceMap 進(jìn)行再映射。比如前面提到的 VSCode Debugger 在調(diào)試時,如果映射的路徑不在項目中就無法編輯,就可以通過 sourceMapPathOverrides 等配置再重新映射。

          正因為前端負(fù)責(zé) SourceMap 的解析,所以我們打的斷點在 SourceMap 解析完成之前是沒法告訴后端正確的地址的。所以如果 SourceMap 加載比較慢,可能后端代碼已經(jīng)執(zhí)行就完了,前端才將斷點信息傳遞過去,就會出現(xiàn)打了斷點但是無效的情況。在 VSCode Debugger 中打斷點提示無效也大概是這個原因,沒有完成 SourceMap 的解析,就無法正確映射。

          好在 Devtools 會將這些斷點信息進(jìn)行緩存,所以在刷新網(wǎng)頁后,能立馬將正確的斷點信息傳遞給后端。所以有些網(wǎng)頁在打了斷點后即使關(guān)閉了網(wǎng)頁后過一段時間再打開,依舊可以看到斷點信息。但 VSCode Debugger 則不會緩存這些信息,其實也不應(yīng)該去緩存。所以在調(diào)試斷開后,再重新調(diào)試,又需要重新進(jìn)行 SourceMap 的加載解析,雖然有 pauseForSourceMap 等配置讓程序等到 Debugger 加載完成 SourceMap 再執(zhí)行,但是目前 VSCode Debugger 整體的加載解析 SourceMap 的效率還是比較低,期待未來能做到更好。

          有興趣可以關(guān)注 vscode-js-debug[10],它就是 VSCode 所使用的 CDP Debugger。

          Vite

          Vite 是目前大火的構(gòu)建工具,相比于傳統(tǒng)的構(gòu)建工具如 Webpack 和 Rollup,Vite 的最大特點是“快”。這得益于 Vite 利用了瀏覽器原生的 ES modules 功能 。具體來說,Vite 會根據(jù)入口文件中的依賴關(guān)系,生成一棵依賴樹,并將各個模塊作為單獨(dú)的文件提供給瀏覽器。也無需單獨(dú)配置 SourceMap 就能映射到源碼。那它是怎么做到的呢?

          Vite 會將每個文件進(jìn)行轉(zhuǎn)換,然后提供給瀏覽器,而轉(zhuǎn)換的文件中就已經(jīng) inline 了 SourceMap,所以我們可以直接對源碼進(jìn)行斷點調(diào)試了。

          Jest

          Jest 作為目前主流的單測工具,它又是怎么做到單測時斷點調(diào)試的呢?其實它和 vite 類似,在實際運(yùn)行代碼前,也會對代碼進(jìn)行轉(zhuǎn)換,并將 SourceMap inline 到轉(zhuǎn)換后的文件中,所以我們也可以直接對源碼進(jìn)行調(diào)試,以如下的 VSCode Debug 配置為例:

          再聊聊 CDP

          CDP 簡單講就是一組 API,用于與 Chrome DevTools 進(jìn)行通信。它允許開發(fā)人員以編程方式控制 Chrome,例如在 Chrome 中打開一個新的選項卡,加載網(wǎng)頁,設(shè)置網(wǎng)絡(luò)條件等。CDP 可以通過 WebSocket 進(jìn)行通信,也可以通過 HTTP 請求進(jìn)行通信。上文內(nèi)容更多的聚焦在代碼調(diào)試這一塊,但是 CDP 遠(yuǎn)不止于此,Chrome DevTools 的大部分功能都是基于 CDP 實現(xiàn)的。

          Puppeteer 是一個著名的自動化庫,用于自動化控制 Chrome 或 Chromium 瀏覽器。本質(zhì)上就是使用 CDP 協(xié)議來與瀏覽器進(jìn)行通信,相當(dāng)于是對 CDP 的高級封裝版。

          基于 CDP,我們可以做很多有趣的事,比如自己打造一個獨(dú)享版的 Devtools,可以使用 Chrome 提供的 chrome-remote-interface[11],它是對 CDP 的 Node.js 封裝,使用起來就像是 Pupeteer 一樣;也可以直接基于 Chrome Devtools 進(jìn)行修改,Chrome 也將 Devtools[12] 倉庫開源了,比如小程序的調(diào)試器就可以基于 Devtools 項目做二次封裝。

          至此就是本文的全部內(nèi)容,希望能對你有所幫助,如有錯誤歡迎指正。

          參考資料

          [1]

          前端調(diào)試通關(guān)秘籍: https://juejin.cn/book/7070324244772716556?utm_source=profile_book

          [2]

          debug-dojo: https://github.com/wjgogogo/debug-dojo

          [3]

          官網(wǎng)文檔: https://chromedevtools.github.io/devtools-protocol/

          [4]

          該文檔: https://peter.sh/experiments/chromium-command-line-switches/

          [5]

          官方文檔: https://code.visualstudio.com/docs/editor/debugging

          [6]

          Recipes: https://code.visualstudio.com/docs/nodejs/debugging-recipes

          [7]

          在線的 SouceMap 可視化工具: https://evanw.github.io/source-map-visualization/

          [8]

          官方文檔: https://webpack.js.org/configuration/devtool/

          [9]

          source-map-loader: https://webpack.js.org/loaders/source-map-loader/

          [10]

          vscode-js-debug: https://github.com/microsoft/vscode-js-debug

          [11]

          chrome-remote-interface: https://github.com/cyrus-and/chrome-remote-interface

          [12]

          Devtools: https://github.com/ChromeDevTools/devtools-frontend


          瀏覽 1099
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  黑人巨大开小嫩苞 | 亚洲天堂成人 | 三级黄色毛片 | sese av | 亚洲AV在线免费观看 |