自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

大型前端項(xiàng)目的斷點(diǎn)調(diào)試共享化和復(fù)用化實(shí)踐

開發(fā) 開發(fā)工具
隨著我們項(xiàng)目越來越大,我們有可能需要維護(hù)很多的模塊,我們騰訊文檔 Excel 項(xiàng)目大模塊有 10 幾個,而每個大模塊分別有 N 個小模塊,每個大模塊下的小模塊都有主要的負(fù)責(zé)人在跟進(jìn)模塊問題。

 [[345256]]

背景

隨著我們項(xiàng)目越來越大,我們有可能需要維護(hù)很多的模塊,我們騰訊文檔 Excel 項(xiàng)目大模塊有 10 幾個,而每個大模塊分別有 N 個小模塊,每個大模塊下的小模塊都有主要的負(fù)責(zé)人在跟進(jìn)模塊問題。

 

這就會導(dǎo)致一個很大的問題是,模塊負(fù)責(zé)人大部分情況只會關(guān)注自己模塊的問題,而不甚了解其他負(fù)責(zé)人手上模塊的具體問題。

比如:當(dāng)我們有用戶反饋使用復(fù)制粘貼有問題的時候,我們想要快速去定位這個問題,就只能找復(fù)制粘貼對應(yīng)的模塊負(fù)責(zé)人處理,如果復(fù)制粘貼模塊負(fù)責(zé)人請假了,那么其他負(fù)責(zé)人去處理這個問題的時候,解決成本就會非常大,因?yàn)槠渌?fù)責(zé)人可能根本對這個模塊不熟悉。

 

又比如:我們新來了幾個同學(xué),想讓他快速去排查用戶反饋的問題的時候,我們只能手把手把我們該模塊調(diào)試的經(jīng)驗(yàn)傳授他,和所熟知的各個坑點(diǎn)告訴他,或者整理好對應(yīng)的 iwiki 給他看(一般效率低也沒人看!),讓他去慢慢定位問題,這樣的每個新同學(xué)對模塊的熟悉,學(xué)習(xí)和維護(hù)的成本就會變得越來越大,項(xiàng)目越大這種情況就會越嚴(yán)重!

 

所以我們思考了很多,該怎么去解決這些問題,至少要讓模塊維護(hù)成本變低,變得更好去維護(hù)和定位問題。

方案

由于上面的問題真的很痛,我們在爬滾中逐漸摸索了一套方案,我們暫且叫它為基于斷點(diǎn)調(diào)試的共享化和復(fù)用化的實(shí)踐方案吧,這里有個關(guān)鍵詞是斷點(diǎn),相比作為每一個開發(fā)者都不陌生,在我們前端,模塊定位問題的時候,我們少不了去使用斷點(diǎn)去斷住一些代碼運(yùn)行關(guān)鍵的地方。下面舉一個例子:

 

  1. class CopyPaste { 
  2.     // 內(nèi)部粘貼 
  3.     pasteFromInter(){ ...} 
  4.     // 外部粘貼 
  5.     pasteFromOuter(){ debugger; ...} 
  6.     // 外部圖文粘貼 
  7.     isShapePasteFromOuter(){ ... } 
  8.     // 外部圖片粘貼 
  9.     isImgPasteFromOuter(){ ... } 
  10.     // 外部文本粘貼 
  11.     isTextFromOuter(){ ... } 

上面這段代碼是當(dāng)用戶反饋一個復(fù)制粘貼問題的時候,熟悉該模塊的負(fù)責(zé)人根據(jù)用戶的反饋,知道用戶是外部粘貼出現(xiàn)了問題,由于他對該模塊熟悉,他會快速的在瀏覽器的控制臺打斷點(diǎn),或者手動在源代碼注入 debugger 關(guān)鍵詞去一步一步定位用戶的問題,他會先檢查內(nèi)部粘貼 pasteFromOuter 是否觸發(fā)了,然后檢查函數(shù) isShapePasteFromOuter 是否運(yùn)行成功,出參和入?yún)⑹欠裾_,是否代碼走歪了,去了 isImgPasteFromOuter。

 

然后在問題排查修復(fù)完后,長舒一口氣,等遇到下一個問題的時候,再把瀏覽器或者代碼中當(dāng)前的這些調(diào)試的痕跡清理干凈,再周而復(fù)始的重復(fù)上面的一系列動作,我相信大部分的同學(xué)每天排查問題甚至做需求都是重復(fù)著上面的類似動作,我們是否可以考慮一下把這些珍貴的調(diào)試痕跡給保存下來,等自己或者其他同學(xué)遇到類似模塊問題的時候,我們把這些凝聚著我們血與淚的心路歷程再自動復(fù)現(xiàn)一次?

代碼片段 記錄 debugger 位置
pasteFromInter 2 行 4 列
isShapePasteFromOuter 256 行 89 列
isImgPasteFromOuter 867 行 12 列

對于大型項(xiàng)目來說,每一個小 Bug 的調(diào)試鏈路的時間成本都是無比巨大的,也是難以復(fù)刻和重現(xiàn)的,我們能做的就是當(dāng)再次遇到相似問題的時候,復(fù)用相似的調(diào)試經(jīng)驗(yàn)。有過受傷的痕跡和經(jīng)歷,當(dāng)問題再次相遇,我們應(yīng)該會更自信和從容。

所以我們首要任務(wù)其實(shí)就變成了是保留珍貴的調(diào)試鏈路,也就是保留無數(shù)個日夜,那些深扎并刺痛我們內(nèi)心深處的每個斷點(diǎn)。

 

插件化

在實(shí)踐的過程中我們嘗試過無數(shù)的方法,第一個方案就是基于瀏覽器插件,實(shí)現(xiàn)斷點(diǎn)留存,基于谷歌瀏覽器插件開發(fā)提供的接口 chrome.debugger,它是 Chrome 遠(yuǎn)程調(diào)試協(xié)議的一種消息傳輸方式。chrome.debugger 可以附加到一個或多個標(biāo)簽頁調(diào)試 JavaScript。并使用調(diào)試對象基于 sendCommand 和 onEvent 來做插件通信。它可以讓我們在插件去調(diào)試頁面,很多插件和工具是基于這個協(xié)議來跟瀏覽器的控制臺去做通信,這種方案現(xiàn)只能實(shí)現(xiàn)一個遠(yuǎn)程的調(diào)試面板,這個面板類似瀏覽器本身的調(diào)試界面可以加載代碼然后記錄斷點(diǎn),最后可以把這些斷點(diǎn)分享出去。

這種方案體驗(yàn)會比較糟糕,首先插件自己實(shí)現(xiàn)的調(diào)試面板無法像谷歌瀏覽器那么好的體驗(yàn),其次是插件需要開發(fā)主動去安裝,分享的前提是雙方都需要安裝好對應(yīng)的插件,開發(fā)和推廣成本都比較高,所以個人不是很建議,但是這不代表這個方案走不通,因?yàn)檫@個基于插件還可以有另外一種實(shí)現(xiàn),就是下面的 debug 函數(shù)方案。

debug 函數(shù)

具體是利用函數(shù)斷點(diǎn) debug(functionName) 和 undebug(functionName) 方法,其中 functionName 是要調(diào)試的函數(shù)。我們可以將 debug() 插入到的代碼中(這個方法和 console.log() 語句相似),也可以從 DevTools 控制臺中進(jìn)行調(diào)用。debug() 相當(dāng)于在第一行函數(shù)中設(shè)置代碼行斷點(diǎn)。

 

一般情況是在控制臺中使用,這個方法配合插件會有比較好的體驗(yàn),因?yàn)椴寮褂?chrome.devtools.inspectedWindow.eval 方法配合瀏覽器的接口可以把代碼注入到控制臺中執(zhí)行,從而實(shí)現(xiàn)幫你自動下發(fā)斷點(diǎn)的功能。

 

  1. chrome.devtools.inspectedWindow.eval( 
  2.   `debug(window.xxxApi);`, 
  3.   (value) => { 
  4.     callback && callback(value); 
  5.   } 
  6. ); 

 

但是細(xì)心的同學(xué)發(fā)現(xiàn)我使用 debug 函數(shù)監(jiān)聽的是一個全局的函數(shù) window.xxxApi,所以這里也總結(jié)一下經(jīng)驗(yàn),這個方法的缺陷就是如果你在控制臺使用,它會在你的上下文尋找該函數(shù),所以它一般只能用于全局的函數(shù)打點(diǎn),如果需要打點(diǎn)的函數(shù)不在上下文,還需要手動斷點(diǎn)到目標(biāo)函數(shù)的范圍,然后使用函數(shù)打點(diǎn)來觸發(fā),如果是閉包函數(shù)那就毫無辦法了,但是瑕不掩瑜,這個方法能幫我們快速定位任何的全局函數(shù),就算代碼被混淆了,它還是能快讀把函數(shù)斷點(diǎn)給你加上,所以這個方案我建議可以作為一個備選方案,在某些情況下能發(fā)揮奇效!

AST 注入

經(jīng)歷過上面的各種坑之后,下面我們簡單介紹我們實(shí)現(xiàn)的一套方案吧:

我們的方案其實(shí)是在之前函數(shù)調(diào)用鏈方案基礎(chǔ)上做的一種改進(jìn),既然我們開發(fā)可以自己在代碼中輸入 debugger 關(guān)鍵詞去斷住任何地方的代碼,我們何不把這個工作交給工具?

 

首先我們可以用使用狀態(tài)機(jī)去告訴工具我們需要分發(fā)的打點(diǎn)的位置在哪里,類似我們常用 whistle 的配置表:

 

  1. Module 'CopyPaste' 
  2.     index.ts -f pasteFromInter -s !(()=>{ console.log(window.Worker) })() 
  3.     index.ts -f pasteFromOuter -s console.log('success') -check messagecenter1 
  4.     index.ts -f isShapePasteFromOuter 
  5. End Module 
  • Module <-- state --> End Module 這里描述一個狀態(tài),是一個分發(fā)斷點(diǎn)的行為,用來需要監(jiān)聽那類模塊的,例如:復(fù)制粘貼模,數(shù)據(jù)層模塊還是數(shù)據(jù)層模塊
  • -f functionname -s code 這里可以描述該狀態(tài)的具體行為特征,例如:在 pasteFromInter 函數(shù)中分發(fā)斷點(diǎn),并注入 debugger 代碼。

在 webpack 中我們可以在 loader 或者 plugin 這兩個過程中去解析這份配置文件,這里你也可以使用第三方庫或者正則來解析上面這些狀態(tài)文本。我是在 loader 中去解析這份狀態(tài)表的,我在全局目錄下或者局部模塊內(nèi)定義一份 .debug.json 來寫入上述的狀態(tài),然后解析出一份 map 對象出來:

 

  1. args = argument({ 
  2.     "--class": String, // 類 
  3.     "--function": String, // 函數(shù) 
  4.     "--code": String, // 函數(shù) 
  5.     "-c""--class", // 轉(zhuǎn)義替換 
  6.     "-f""--function"
  7.     "-s""--code"
  8.   },{ argv: debugConfigValue, } 
  9. ); 

如果不想用狀態(tài)機(jī)的方式去寫配置文件的話,其實(shí)也可以使用一份 debug.json 文件來描述斷點(diǎn)的位置,這種方式更簡單,解析 json 文件的成本比狀態(tài)機(jī)的配置文件低不少,json 文件在這里涉及的主要字段分別是需要檢測代碼的路徑,這個方便工具去定位文件,然后是需要檢測的類或者函數(shù)的名字,這個方便工具去定位代碼的位置,還有檢測項(xiàng)的名字和需要檢測的代碼,和一個關(guān)鍵的鍵值:

 

  1.   "MessageCenter": { 
  2.     "function": [ 
  3.       { 
  4.         "path""src/core/network/message-center/SendMessageCenter.ts"
  5.         "name""_sendUserChanges"
  6.         "title""數(shù)據(jù)層斷點(diǎn)測試2"
  7.         "code""__console.log('數(shù)據(jù)層斷點(diǎn)測試2')"
  8.         "key""MessageCenter|function|1" 
  9.       } 
  10.     ] 
  11.   } 

這里鍵值的涉及可以定義的清晰點(diǎn),比如 MessageCenter|function|1 指的是對 MessageCenter 模塊的文件里面的某一個函數(shù)打點(diǎn),以后還可以繼續(xù)改進(jìn)這樣寫 MessageCenter|class|1:12,意思是 MessageCenter 模塊的文件里面某一個類的具體位置打點(diǎn),如果這個 key 的語義越豐富,后續(xù)分發(fā)的打點(diǎn)也會更精確,定位問題也會更高效,具體這個可以根據(jù)業(yè)務(wù)場景去定義。

 

  1. class CopyPaste { 
  2.     // 內(nèi)部粘貼 
  3.     pasteFromInter(){ 
  4.         debugger 
  5.         ... 
  6.     } 

當(dāng)我們有了配置文件,我們就得思考怎么無入侵的在代碼里面加入調(diào)試和檢測代碼了,我們首選通過 AST 去注入,它可以幫我們把代碼關(guān)鍵部分給梳理成一顆樹出來,比如抹掉冒號、括號、分號等,能讓我們把精力放在重要的節(jié)點(diǎn)上,上面的代碼經(jīng)過解析會得到下面這棵 AST 語法樹:

 

  1.   "program": { 
  2.     "type""Program"
  3.     "body": [{ 
  4.       "type""ClassDeclaration"
  5.       "id": {{ "type""Identifier""identifierName""CopyPaste" }, "name""CopyPaste" }, 
  6.       "body": { 
  7.         "type""ClassBody"
  8.         "body": [{ 
  9.             "type""ClassMethod"
  10.             "key": { "type""Identifier""name""pasteFromInter" }, 
  11.             "body": { "type""BlockStatement""body": [{ "type""DebuggerStatement" }]}, 
  12.             "leadingComments": [{ "type""CommentLine""value"" 內(nèi)部粘貼" }], 
  13.         }] 
  14.       } 
  15.     }] 
  16.   } 

而具體步驟大概如下:解析 MessageCenter|function|1 這段參數(shù)配置的字符串,得到函數(shù)名,模塊名,位置信息等,然后對代碼進(jìn)行掃描并進(jìn)行詞法和語法分析,并得到 AST 語法樹,根據(jù)剛才解析得到的函數(shù)名,模塊名,位置信息來匹配 AST 樹節(jié)點(diǎn),在上面進(jìn)行加入我們的調(diào)試和檢測代碼,最后再輸出經(jīng)過我們加工的代碼。

那上面這個原理我們都懂,具體怎么實(shí)現(xiàn)呢,我們可以在 webpack 工具使用 plugins 來實(shí)現(xiàn),在 plugins 中我們經(jīng)常會用到訪問者模式,就是說在訪問到某一個路徑的時候進(jìn)行匹配,然后在對這個節(jié)點(diǎn)進(jìn)行修改,比如上面這個 pasteFromInter 函數(shù),它是一個 ClassMethod,plugins 就會對代碼生成的 AST 樹進(jìn)行訪問,訪問者可以匹配任何對應(yīng)的詞法特性,我們就可以在這里匹配所有的 ClassMethod 然后根據(jù)路徑去拿到節(jié)點(diǎn)對應(yīng)的信息,比如函數(shù)名,函數(shù)參數(shù)和函數(shù)位置等,拿到這些關(guān)鍵的信息,我們就可以對這個函數(shù)節(jié)點(diǎn)進(jìn)行加工,也就是注入我們的調(diào)試和檢測代碼或者直接注入一個 debugger 去打斷點(diǎn)。

 

  1. plugins = { 
  2.   // 訪問器 
  3.   Visitor = { 
  4.       'ClassMethod'(path) { 
  5.         // 檢點(diǎn) 
  6.         path.node 
  7.       } 
  8.   } 

當(dāng)然注入檢測代碼也是需要構(gòu)造成 ClassMethod 的類似結(jié)構(gòu),所有我們可以配合 @babel/types 工具去快速注入一段代碼,比如最簡單的是注入一個 debugger:

 

  1. types.expressionStatement(types.identifier(`debugger`)) 

這樣就會在你匹配的路徑的特定位置放入一個 debugger,而你的代碼源文件本身其實(shí)是沒有任何改動的,只是通過 AST 樹配合配置文件成功融合了一段代碼到指定的位置,當(dāng)然實(shí)際情況會比預(yù)想中的復(fù)雜,因?yàn)橛锌赡芟掳l(fā)的位置不是函數(shù)中的某個位置,可能是類函數(shù)中的某個位置,閉包函數(shù)中的某個位置,所以要兼容各種的語法結(jié)構(gòu),需要在 AST 中匹配這些函數(shù)的所有特征才能準(zhǔn)確無誤的下發(fā)代碼,還是以函數(shù)作為例子,列出部分需要考慮的情況:

  • FunctionExpression

需要滿足到這兩種寫法,不然 debugger 會下發(fā)錯位置。

 

  1. this.xxx = function() { debugger } 
  2. const xxx = function() { debugger } 
  • ClassMethod

這個一般情況按下面的方式就能定位到了,但是如果要更精確比如是私有函數(shù)等,那就需要寫更精確的訪問器了。

 

  1. class xxx { xxx:(){ debugger } } 
  • FunctionDeclaration

除了要處理上面函數(shù)表達(dá)式的寫法,不要忘了函數(shù)還有聲明定義的寫法,所以這個也得滿上。

 

  1. function xxx() { debugger } 
  • ArrowFunctionExpression

最后還要考慮下箭頭函數(shù)的寫法

 

  1. const xxx = () => { debugger } 
  2. this.xxx = () => { debugger } 
  3. class xxx { xxx = () => { debugger } } 

雖然大部分情況匹配函數(shù)對項(xiàng)目下發(fā)的調(diào)試代碼能覆蓋大部分的場景,但總會有漏網(wǎng)之魚,比如有的同學(xué)想在類定義之前注入檢測代碼,那就需要繼續(xù)寫對應(yīng)的訪問器去獲取路徑,然后對該位置去分發(fā)對應(yīng)的檢測代碼,所以需要對各種語法和對應(yīng)的訪問器類型很熟悉才能順利實(shí)現(xiàn)。

經(jīng)過上面的改造,我們會在最終代碼中會得到新代碼(已注入了所有檢測代碼),但是這樣會引發(fā)一個新的,當(dāng)我們運(yùn)行這份新代碼,我們上面所有的檢測代碼都會跑一遍,這樣就會斷住很多別的模塊負(fù)責(zé)人不想斷住的代碼區(qū)域,所以實(shí)際情況我們需要分發(fā)一個帶開關(guān)的檢測代碼,當(dāng)然這個開關(guān)的涉及其實(shí)可以很簡單,如下:

 

  1. // 基于 AST 在模塊中分發(fā)的調(diào)試開關(guān) 
  2. if(require('@tencent/vdebugger').call(this, key)){ debugger } 
  3. // 或者這樣,雖然好看點(diǎn),但這樣 debugger 在閉包里面拿不到上下文 
  4. require('@tencent/vdebugger').call(this, key) || (() => { debugger })() 
  5. // 注意這種下面類似這種寫法是不行的↓ 
  6. require('@tencent/vdebugger') || debugger 

我們可以使用 require('@tencent/vdebugger') 打包一個函數(shù),這個函數(shù)可以設(shè)計為在全局變量或者 localstorage 等地方讀取配置,然后返回一個布爾值,用于判斷是否執(zhí)行該位置的 debugger,這里為了調(diào)試方便有幾個小細(xì)節(jié)需要注意,debugger 這個關(guān)鍵詞自己要獨(dú)立一個作用域,所以你不能寫成類似這個樣子 false || debugger,還有 require('@tencent/vdebugger') 這個函數(shù)里面在讀取配置之后里面可以包一個 eval 方法來執(zhí)行檢測代碼,所以可以用 call 把當(dāng)前作用域代理過來,更方便去做調(diào)試。

當(dāng)然實(shí)際情況可能還要比想象中復(fù)雜,舉個簡單的例子:因?yàn)榉职l(fā)的開關(guān)有可能會注入到一些被打包到 worker 的代碼里面,worker 在大型項(xiàng)目中運(yùn)用的很多,但是 worker 里面無法讀取 document、window 這些對象,雖然可以使用 navigator,location 和 XMLHttpRequest 等對象,但無法通過 localstorage 讀取配置等手段去控制調(diào)試開關(guān)了,所以你需要考慮一下是否需要讓調(diào)試開關(guān)分發(fā)到 worker 代碼中,如果分發(fā)了又要怎么去通信對應(yīng)的開關(guān)等問題。

最簡單粗暴就是打包 worker 代碼的時候進(jìn)行過濾。

 

  1. !isWorker && new DebuggerPlugin({ 
  2.     debugConfig: path.resolve(dirName, '../debug.json'), 
  3. }), 

當(dāng)然如果需要分發(fā)的開關(guān)在 worker 中生效,就需要去實(shí)現(xiàn)一個讀取開關(guān)配置的通信手段,最常見的就是基于 postMessage 的通信手段,讓 require('@tencent/vdebugger') 函數(shù),即開關(guān)模塊接受主線程的配置去向 worker 的運(yùn)行代碼下達(dá)是否執(zhí)行檢測代碼和啟動斷點(diǎn)的命令。

 

  1. myWorker.postMessage(xx); 
  2. myWorker.onmessage = () => { 
  3.   console.log('Message received from worker'); 

思考

實(shí)現(xiàn)了上面的基本功能之后,我們還可以繼續(xù)優(yōu)化很多體驗(yàn),比如我們還可以使用 webpack 的 plugin 來實(shí)現(xiàn)本地編譯時候的增量更新,這就能做到當(dāng)我們更改本地配置文件的時候,自動分發(fā)斷點(diǎn)和調(diào)試代碼,邏輯也是比較簡單的,在 plugin 的 apply 周期使用內(nèi)置的庫 chokidar 去監(jiān)聽配置文件的變更,然后觸發(fā)編譯,重新走 AST 去編譯生成帶調(diào)試代碼合斷點(diǎn)的代碼:

 

  1. const chokidar = require('chokidar'); 
  2. this.watcher = chokidar.watch(["../src/**/.debug.json"], { 
  3.   usePolling: true
  4.   ignored: this.options.ignored 
  5. }); 

總結(jié)

關(guān)于這方面的調(diào)試相關(guān)文章不多,一路走來跳了不少的坑,感謝團(tuán)隊(duì)成員的支持,并讓這個方案最終成功落地,也希望有更多志同道合的人加入我們騰訊文檔團(tuán)隊(duì),一起去探索和遨游,最后也希望這篇文章能給到你們一些啟發(fā)吧 。

【本文為51CTO專欄作者“騰訊技術(shù)工程”原創(chuàng)稿件,轉(zhuǎn)載請聯(lián)系原作者(微信號:Tencent_TEG)】

戳這里,看該作者更多好文

責(zé)任編輯:武曉燕 來源: 51CTO專欄
相關(guān)推薦

2022-10-09 14:50:24

前端pnpm工具

2023-09-07 20:04:06

前后端趨勢Node.js

2021-09-27 10:48:42

開發(fā)技能代碼

2021-09-27 09:04:40

Vue.js代碼庫開發(fā)人員

2022-05-09 09:28:04

Vite前端開發(fā)

2020-09-22 12:20:23

前端架構(gòu)插件

2024-02-21 09:32:18

開發(fā)架構(gòu)

2023-04-07 15:12:46

ReactReact-Intl

2024-12-26 08:00:38

2023-03-07 08:30:09

MCube模板緩存

2022-08-10 09:52:16

平臺實(shí)踐

2015-12-08 09:13:05

開發(fā)維護(hù)Java項(xiàng)目

2021-01-25 10:30:52

數(shù)字化分析轉(zhuǎn)型首席執(zhí)行官

2022-08-10 10:32:47

編程實(shí)踐

2018-10-29 12:21:21

源碼前端項(xiàng)目

2023-06-03 08:06:20

項(xiàng)目開發(fā)客戶端

2010-08-02 08:54:53

Flex模塊化

2021-06-29 10:01:56

物聯(lián)網(wǎng)項(xiàng)目eSIM物聯(lián)網(wǎng)

2018-06-12 15:55:44

數(shù)字化項(xiàng)目

2013-11-27 11:34:43

自動化部署Python
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號