和語(yǔ)言功能一樣,VS Code 是把調(diào)試功能的最終實(shí)現(xiàn)交給插件來(lái)完成的。VS Code 提供了一套通用的圖形界面和交互方式,比如怎么創(chuàng)建斷點(diǎn)、如何添加條件斷點(diǎn)、如何查看當(dāng)前調(diào)試狀態(tài)下參數(shù)的值,等等。無(wú)論你使用哪個(gè)編程語(yǔ)言或者調(diào)試器,這一套交互流程都是相似的。
而對(duì)于插件作者而言,他們需要完成的是如何把真正的調(diào)試工作跟 VS Code 的界面和交互結(jié)合起來(lái),為此 VS Code 為插件作者提供了一套統(tǒng)一的接口,叫做Debug Adapter Protocol(DAP)。當(dāng)用戶在界面上完成一系列調(diào)試相關(guān)的操作時(shí),VS Code 則通過(guò) DAP 喚起調(diào)試插件,由插件完成最終的操作。
講到這里,你可能想到了,如果你在使用的語(yǔ)言已經(jīng)有一個(gè)命令行的調(diào)試工具,那你也可以通過(guò)寫(xiě)一個(gè)調(diào)試插件,把這個(gè)命令行的調(diào)試器通過(guò) DAP 連接到 VS Code 中,然后就能夠借助 VS Code 這套 UI 來(lái)進(jìn)行圖形化的調(diào)試了。沒(méi)錯(cuò),調(diào)試插件很大程度上就是在進(jìn)行這樣的 “翻譯” 工作。下面這張 VS Code DAP 的流程圖也很好地做出了解釋:
當(dāng)然,盡管我們?cè)诰庉嬈髦刑峁┝烁鞣N調(diào)試的界面和功能,但這并不意味著每一個(gè)調(diào)試插件把它們?nèi)紝?shí)現(xiàn)了。這可能是因?yàn)椴寮€沒(méi)有足夠成熟,也有可能是受限于底層的調(diào)試器。
首先,讓我們一起來(lái)看下 VS Code 的通用調(diào)試界面。今天我會(huì)以 Node.js 為主要的語(yǔ)言來(lái)介紹,對(duì)于任何編程背景的同學(xué)來(lái)說(shuō),這都沒(méi)有什么難度。
在進(jìn)入調(diào)試知識(shí)之前,我們可以先做一些準(zhǔn)備,在當(dāng)前目錄下創(chuàng)建一個(gè)文件 index.js ,內(nèi)容如下:
function foo() {
bar("Hello World");
}
foo()
function bar(str) {
console.log(str);
}
JavaScript
VS Code 中有一個(gè)專門(mén)的用于管理調(diào)試功能的視圖。我們可以點(diǎn)擊界面左側(cè)“昆蟲(chóng)”(也就是 bug 啦)形狀的按鈕,或者按下 “Cmd + Shift + D” (Windows 上是 Ctrl + Shift + D)來(lái)喚出調(diào)試視圖。
在視圖的最上側(cè),有個(gè)綠色的箭頭按鈕。這個(gè)按鈕是用于啟動(dòng)調(diào)試器的。但是在上面的截圖里,你可以看到在綠色箭頭的右側(cè)寫(xiě)著 “沒(méi)有配置”。這說(shuō)明現(xiàn)在 VS Code 還不知道該使用什么調(diào)試器來(lái)調(diào)試當(dāng)前的代碼。此時(shí)點(diǎn)擊這個(gè)按鈕或者按下 F5,我們能夠看到一個(gè)列表。
這個(gè)列表有兩個(gè)選項(xiàng),一個(gè)是 Chrome,另一個(gè)則是 Node.js。其中,Node.js 的調(diào)試器是 VS Code 默認(rèn)就支持的,而 Chrome 這個(gè)選項(xiàng)則是因?yàn)槲野惭b了 Chrome 調(diào)試相關(guān)的插件 Debugger for Chrome – Visual Studio Marketplace 。 為了便于理解,這里我會(huì)選擇 Node.js 。
選擇完 Node.js 后,我們可以看到 VS Code 的 UI 發(fā)生了變化,但是一閃而過(guò),然后又恢復(fù)了正常。這是因?yàn)?VS Code 的 Node.js 調(diào)試器發(fā)現(xiàn)本地有 index.js 文件,于是直接執(zhí)行了這個(gè)文件;index.js 文件內(nèi)容非常簡(jiǎn)單,很快結(jié)束了。
下面,讓我們放慢速度再來(lái)一次。
首先,我們將鼠標(biāo)移動(dòng)到第五行代碼的行號(hào)前面,點(diǎn)擊鼠標(biāo)左鍵,我們能夠看到一個(gè)紅色的圓點(diǎn)被創(chuàng)建了出來(lái),這就是斷點(diǎn)。當(dāng)然,我們也可以把光標(biāo)移動(dòng)到第五行,然后按下 F9,同樣可以在第五行創(chuàng)建斷點(diǎn)。
此時(shí),當(dāng)我們?cè)俅吸c(diǎn)擊調(diào)試視圖上面的綠色箭頭按鈕,或者按下 F5,啟動(dòng)調(diào)試器,并且選擇 Node.js ,VS Code 就會(huì)進(jìn)入調(diào)試模式。
我們能夠看到界面中間出現(xiàn)了一個(gè)工具欄,用于控制代碼的執(zhí)行;左側(cè)的調(diào)試視圖,現(xiàn)在也展示了當(dāng)前上下文里的變量、調(diào)用堆棧和所有創(chuàng)建的斷點(diǎn)等。相信這些你早就已經(jīng)非常熟悉了,這里我就不多加贅述。
但是,如果工作區(qū)并沒(méi)有任何打開(kāi)的文件,那當(dāng)我們?cè)俅伟聪?F5 進(jìn)行調(diào)試,然后選擇 Node.js 時(shí),VS Code 會(huì)告訴我們 “找不到要調(diào)試的程序”。這句話什么意思呢?
在沒(méi)有任何配置的情況下,VS Code 的 Node.js 調(diào)試器,會(huì)嘗試著去調(diào)試當(dāng)前打開(kāi)的文件,而如果當(dāng)前沒(méi)有任何的文件被打開(kāi)的話,Node.js 調(diào)試器就不知道該調(diào)試哪個(gè)代碼文件了。而且很多時(shí)候,我們的項(xiàng)目相對(duì)比較復(fù)雜,單個(gè)文件的調(diào)試還是相對(duì)太理想化了。為了解決這個(gè)問(wèn)題,我們需要給 VS Code 提供一個(gè)配置文件,告訴調(diào)試器如何加載和調(diào)試代碼。
相信你對(duì) VS Code 的各種配置文件早就不陌生了,VS Code 用于配置代碼調(diào)試的文件跟其他的很類似,是一個(gè) JSON 文件,叫做 launch.json 。我們可以把它放在 .vscode 文件夾下,也能夠?qū)⑺膬?nèi)容放在個(gè)人或者工作區(qū)的配置文件里。
VSCode 代碼調(diào)試器配置launch.json介紹,在調(diào)試視圖的最上方,我們能夠看到一個(gè)齒輪形狀的按鈕,它可以用于創(chuàng)建和修改 launch.json 文件。由于當(dāng)前文件夾下沒(méi)有 launch.json 文件,所以這個(gè)按鈕的右上角有個(gè)紅色的點(diǎn),它告訴我們當(dāng)前的調(diào)試配置有一點(diǎn)問(wèn)題,讓我們點(diǎn)擊這個(gè)按鈕。
當(dāng)我們按下按鈕后,VS Code 詢問(wèn)我們想要?jiǎng)?chuàng)建什么項(xiàng)目的調(diào)試配置,這里我們?cè)俅芜x擇 Node.js。然后我們就能夠看到 .vscode 文件夾下 launch.json 文件被創(chuàng)建出來(lái)了,它的內(nèi)容如下:
{
// 使用 IntelliSense 了解相關(guān)屬性。
// 懸停以查看現(xiàn)有屬性的描述。
// 欲了解更多信息,請(qǐng)?jiān)L問(wèn): https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "啟動(dòng)程序",
"program": "${file}"
}
]
}
JSON
這個(gè) JSON 文件里的 configurations 的值就是當(dāng)前文件夾下所有的配置了?,F(xiàn)在我們只有一個(gè)調(diào)試配置,它有四個(gè)屬性:
第一個(gè)是 type,代表著調(diào)試器的類型。它決定了 VS Code 會(huì)使用哪個(gè)調(diào)試插件來(lái)調(diào)試代碼。
第二個(gè)是 request,代表著該如何啟動(dòng)調(diào)試器。如果我們的代碼已經(jīng)運(yùn)行起來(lái)了,則可以將它的值設(shè)為 attach,那么我們則是使用調(diào)試器來(lái)調(diào)試這個(gè)已有的代碼進(jìn)程;而如果它的值是 launch,則意味著我們會(huì)使用調(diào)試器直接啟動(dòng)代碼并且調(diào)試。
第三個(gè)屬性 name,就是這個(gè)配置的名字了。
第四個(gè)屬性 program,就是告訴 Node.js 調(diào)試器,我們想要調(diào)試哪個(gè)文件。這個(gè)值支持預(yù)定義參數(shù),比如在上面的例子里,我們使用了${file},也就是當(dāng)前編輯器里打開(kāi)的文件。
不過(guò)使用這個(gè)配置,并沒(méi)有解決剛才上面我提的問(wèn)題,如果所有文件都被關(guān)閉了,那么${file} 就是空的了,這個(gè)調(diào)試配置并不能正確運(yùn)行。
下面我們把 program 的值改為 ${workspaceFolder}/index.js,其中${workspaceFolder} 是代表當(dāng)前工作區(qū)文件夾地址的預(yù)定義參數(shù),使用它就能夠準(zhǔn)確地定位當(dāng)前工作區(qū)里 index.js 文件了。(關(guān)于在配置文件里可以使用的預(yù)定義參數(shù),請(qǐng)參考Visual Studio Code Variables Reference。 )
到這一步,即使我們關(guān)閉掉編輯器里全部的文件,當(dāng)我們按下 F5 ,也能夠?qū)?nbsp;Node.js 代碼調(diào)試起來(lái)了。
VSCode 代碼調(diào)試器配置launch.json開(kāi)發(fā),launch.json 該怎么寫(xiě),看到這里,你可能會(huì)說(shuō),“上面講的我都會(huì),快趕緊告訴我該怎么掌握 launch.json 吧,它實(shí)在是太難寫(xiě)了?!睕](méi)錯(cuò),VS Code 的調(diào)試交互方式,跟其他工具并沒(méi)有太大的出入,但是配置起來(lái)可就麻煩了,我也深受其擾。究其原因,不同調(diào)試器在調(diào)試代碼或者工程時(shí),需要的信息各不相同,VS Code 并沒(méi)有辦法為它們統(tǒng)一所有的配置項(xiàng)。
舉個(gè)例子,在上面的 Node.js 調(diào)試配置里,有個(gè)屬性叫做 request,它控制著我們運(yùn)行調(diào)試器時(shí)是 launch 還是該 attach 。對(duì)于絕大部分調(diào)試器,這個(gè)屬性都是有用的,所以 VS Code 預(yù)先定義好了 request 這個(gè)屬性,然后要求每個(gè)調(diào)試配置都必須包含 request 這個(gè)屬性,而且它的值必須是 launch 或者 attach 之一。
我們?cè)诰庉嬈骼锎蜷_(kāi) launch.json 這個(gè)文件時(shí),能夠看到request 這個(gè)屬性的顏色是灰色的,這說(shuō)明它是 VS Code 預(yù)先定義好的屬性,每個(gè)調(diào)試器插件都會(huì)按照一樣的方式去閱讀和理解它的值。
而 name 和 program 這兩個(gè)屬性,它們的顏色是藍(lán)色的,這意味著它們的定義和最終解釋,都是由調(diào)試插件控制的,而 VS Code 并不會(huì)對(duì)它們做任何的約束和處理。
相信到這里,你已經(jīng)明白了 launch.json 的本質(zhì),以及它的書(shū)寫(xiě)難度來(lái)自于哪里。VS Code 提供了調(diào)試界面,但是并沒(méi)有將調(diào)試配置統(tǒng)一起來(lái),而是將它的自由度完全交給調(diào)試器本身,我們?cè)?launch.json 里書(shū)寫(xiě)的調(diào)試配置,其實(shí)就是調(diào)試器的配置或者參數(shù),只不過(guò)它的格式是 JSON。
但是別怕,我們有辦法降低書(shū)寫(xiě)它的難度。我們可以借助VS Code的調(diào)試器插件提供的模板,以及自動(dòng)補(bǔ)全功能。
下面我們打開(kāi) launch.json ,在第十二行最后按下Ctrl + Space或者執(zhí)行 “觸發(fā)建議”這個(gè)命令,VS Code 立刻就會(huì)為我們喚出了建議列表。
建議列表里的,就是調(diào)試插件們給我們提供的調(diào)試配置的模板了。模板的前綴就是它所屬的語(yǔ)言或者插件名稱,后面則是這個(gè)模板的類型。它們一般都有一段說(shuō)明,介紹它大概是完成什么調(diào)試工作的。
比如我已經(jīng)安裝了 Java 的插件,當(dāng)我選擇了 “Java Attach to Remote Program” 這一項(xiàng)時(shí),我們能夠看到它的描述是:
“Add a new configuration for attaching to a running java program” ,它的意思是這個(gè)配置是用于將調(diào)試器 attach 到正在運(yùn)行的 Java 程序上的。
再或者我選擇了 “Node.js: Gulp 任務(wù)“,它的作用是調(diào)試 Gulp 任務(wù)(Gulp 是 Node.js 的一個(gè)任務(wù)腳本工具),同時(shí)它還提示了我要確保項(xiàng)目里已安裝本地 Gulp 腳本。
就是這樣,很多時(shí)候,模板可以幫助我們完成大部分的工作,然后我們只需要稍作修改就可以了。
另一個(gè)能夠幫助到我們的,就是在書(shū)寫(xiě)配置屬性的時(shí)候使用自動(dòng)補(bǔ)全功能。當(dāng)我們?cè)跁?shū)寫(xiě)新的屬性時(shí),按下 Ctrl + Space,就能夠喚出建議列表,建議列表里提供了當(dāng)前調(diào)試配置里可以使用的所有屬性,然后我們就可以按需選用了。
雖然每個(gè)調(diào)試器各自控制著用戶可以使用哪些屬性,但是調(diào)試器之間還是有很多相同的地方,調(diào)試插件在很多時(shí)候都會(huì)使用相同的屬性名來(lái)代表同樣的功能。比如,我自己就是 Ruby 插件的作者,我在實(shí)現(xiàn) Ruby 調(diào)試插件的時(shí)候,參考了很多 Node.js 和 PHP 調(diào)試插件對(duì)于屬性的命名和使用。我在書(shū)寫(xiě)不同語(yǔ)言的調(diào)試配置時(shí),經(jīng)常使用的有下面這些:
在任務(wù)系統(tǒng)配置里,我們介紹過(guò)如何為不同的操作系統(tǒng)指定不同的配置,調(diào)試配置也支持這樣的語(yǔ)法。比如 program 這個(gè)屬性,我們可以默認(rèn)使用 macOS 或者 Linux 的書(shū)寫(xiě)方式 :
"program": "path/app.js"
JSON
然后我們也可以為 Windows 平臺(tái)指定特定的書(shū)寫(xiě)方式:
"windows": {
"program": "path\\app.js"
}
JSON
上面我建議的書(shū)寫(xiě)調(diào)試配置 launch.json 的方法,總結(jié)來(lái)說(shuō)就是:第一,借助模板和智能提示,盡可能利用調(diào)試插件給我們的提示和文檔;第二,試著記住和學(xué)習(xí)通用的配置屬性和技巧,有些知識(shí)我們?cè)趯W(xué)習(xí)任務(wù)系統(tǒng)時(shí)也已經(jīng)涉獵過(guò)了。
但在我自己學(xué)習(xí)配置 launch.json 的過(guò)程中,我自己體悟的最重要的一條方法,就是去試著理解調(diào)試器本身,或者講的更寬泛一些,理解自己正在執(zhí)行的操作究竟干了什么。
尤其是當(dāng)我們已經(jīng)熟悉了通用的那些屬性的配置,如果配置完它們都還不能工作,那么很有可能是我們使用的這個(gè)調(diào)試器有一些特殊的要求。這里我舉兩個(gè)例子。
第一個(gè)是我在調(diào)試 Gulp 時(shí)遇到的。Gulp 是一個(gè)自動(dòng)化腳本工具,基于 Node.js ,可以通過(guò) NPM 進(jìn)行安裝使用 (npm install gulp)。它的配置文件也是一個(gè) JavaScript 文件,我可以在文件夾下創(chuàng)建 gulpfile.js ,然后在這個(gè)文件里創(chuàng)建多個(gè)不同的任務(wù),之后就可以在命令行里使用 gulp task直接運(yùn)行任務(wù)了。
我最開(kāi)始只知道如何為普通的 JavaScript 文件創(chuàng)建調(diào)試配置,也就是我們?cè)谖恼麻_(kāi)頭提到的方法,為 Node.js 這個(gè)調(diào)試器指定一個(gè)文件,比如 ${workspaceFolder}/index.js,Node.js 調(diào)試器就回去調(diào)試這個(gè)文件了。但是如果我要調(diào)試我寫(xiě)在 gulpfile.js 文件里的某個(gè)任務(wù),該怎么做呢?直接調(diào)試 ${workspaceFolder}/gulpfile.js 代碼并不奏效,因?yàn)槲蚁M{(diào)試的其實(shí)是 gulp task這個(gè)命令。
但是我退過(guò)來(lái)想一下,Gulp 是使用 Node.js 實(shí)現(xiàn)的工具,它的執(zhí)行文件其實(shí)也是一個(gè) JavaScript 代碼(也就是 node_modules/gulp/bin/gulp.js)。gulp task運(yùn)行的時(shí)候,其實(shí)是在 Node.js 下運(yùn)行 node_modules/gulp/bin/gulp.js這個(gè)文件,然后傳入了 task這個(gè)參數(shù)。
理解了這層關(guān)系,那么調(diào)試配置就好寫(xiě)了。我要使用的調(diào)試器是 node,調(diào)試的是 gulp.js 這個(gè)文件,它存在當(dāng)前目錄下的 node_modules 文件夾下,那么我需要使用 ${workspaceFolder}這個(gè)預(yù)定義的變量,同時(shí)給 args傳入 task這個(gè)參數(shù)。最終的調(diào)試配置如下:
{
"type": "node",
"request": "launch",
"name": "Gulp task",
"program": "${workspaceFolder}/node_modules/gulp/bin/gulp.js",
"args": [
"task"
]
}
JSON
第二個(gè)例子則是我在調(diào)試 JavaScript 代碼遇到的。前端項(xiàng)目為了保證加載和運(yùn)行速度,很多都會(huì)對(duì) JavaScript 代碼進(jìn)行打包和壓縮。經(jīng)壓縮的代碼幾乎是不可讀的,為了便于調(diào)試,打包工具都還會(huì)生成一個(gè)特殊的 sourcemap 文件,這個(gè)文件里記錄的是原始代碼和壓縮代碼之間的對(duì)應(yīng)關(guān)系。有了這個(gè)文件,我就能夠像調(diào)試原始代碼一樣調(diào)試經(jīng)壓縮的代碼了。在自動(dòng)補(bǔ)全的提示下,我可以添加一個(gè)屬性 “sourceMaps” ,這樣 JavaScript 的調(diào)試器就知道去閱讀 sourcemap 文件了。
但問(wèn)題來(lái)了,當(dāng)我使用 webpack 打包代碼然后進(jìn)行調(diào)試時(shí),斷點(diǎn)停下的位置總是錯(cuò)得離譜。很顯然,調(diào)試器沒(méi)有能夠正確地通過(guò) sourcemap 文件定位斷點(diǎn)。不過(guò)尋找解決方案的方式還算很簡(jiǎn)單,我到對(duì)應(yīng)的調(diào)試器的 GitHub 代碼倉(cāng)庫(kù) issues 里搜索 “webpack sourcemap” 就找到一摸一樣的問(wèn)題了,原因是 webpack 自己的特殊的 sourcemap 生成方式跟 VS Code 不兼容。要解決這個(gè)問(wèn)題,我們既可以通過(guò)配置 webpack,也可以通過(guò) “sourceMapPathOverrides”這個(gè)屬性來(lái)修復(fù)。
更多建議: