Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
【CSDN 編者按】Visual Studio Code(以下簡稱 VS Code)是一個(gè)強(qiáng)大的工具,可惜很多人卻沒有找到它的正確打開方式。今天就教大家3個(gè)好用易學(xué)的小技巧,讓你的VS Code使用起來如行云流水!
原文鏈接:https://medium.com/fractions/3-visual-studio-code-tips-to-boost-your-workflow-b107ec573d75
聲明:本文為 CSDN 翻譯,轉(zhuǎn)載請(qǐng)注明來源。
以下為譯文:
只要你知道如何使用VS Code,它就是一個(gè)萬能的工具。
隨著時(shí)間的推移,VS Code變得越來越好,并添加了更多的特性。然而,這些特性通常都隱藏在VS Code的JSON設(shè)置中,大多數(shù)新手根本就無法找到。今天,我將與大家分享3個(gè)不同尋常的技巧,它們可以幫助大家提升開發(fā)效率。
配置文件又名dotfiles,是開發(fā)中不可或缺的一部分,因?yàn)楝F(xiàn)在已經(jīng)不是2000年,沒有人再使用普通的HTML、CSS和JavaScript了。我們現(xiàn)在幾乎有了做任何事情的工具,有轉(zhuǎn)譯器、編譯器、綁定器、編譯器、美化器……幸運(yùn)的是,我們可以根據(jù)項(xiàng)目的需要,用配置文件對(duì)它們進(jìn)行配置。
然而,在根目錄中有幾十個(gè)配置文件會(huì)導(dǎo)致一團(tuán)亂。盡管這些可定制的工具非常棒,但在配置它們一次之后,我從來沒有打開它們,除非在項(xiàng)目中有我無法預(yù)見的東西。那么,為什么我每次都要在這個(gè)混亂的文件夾中找到我的主文件夾呢?
幸運(yùn)的是,VS Code有一個(gè)還在實(shí)驗(yàn)階段的設(shè)置功能,叫做fileNesting。它允許開發(fā)者可視化地將文件嵌套到另一個(gè)文件中,并清除工作區(qū)。好在它不會(huì)打亂文件結(jié)構(gòu),而且所有的預(yù)配置工具都可以在沒有任何額外努力的情況下繼續(xù)工作。
對(duì)于這個(gè)項(xiàng)目,我將把我所有的配置文件放在package.json文件和README.md下的變更日志和許可證。
有了這個(gè)設(shè)置,我終于可以找到任何我第一眼想要的,如果我需要編輯任何配置文件,我可以展開像package.json的一個(gè)文件夾,并編輯它下面的文件。
對(duì)于這個(gè)技巧,你必須向setting.json中添加三個(gè)條目。按Ctrl或Cmd + Shift + P打開它,并寫入“settings.json”。然后將這些條目添加到末尾。
"explorer.experimental.fileNesting.enabled": true,
"explorer.experimental.fileNesting.expand": false,
"explorer.experimental.fileNesting.patterns": {
// Append as many as you want
// The keys are the parents and the values are the nested files.
"package.json": ".gitignore, .parcelrc, .prettierc ...",
"README.md": "CHANGELOG.md, LICENCE"
}
就是這樣!沒有更多的混亂的根,所有都容易查找。不要忘記查看并啟動(dòng)我截屏的這個(gè)項(xiàng)目,以確認(rèn)文件結(jié)構(gòu)沒有改變。
不需要擴(kuò)展
擴(kuò)展太棒了!它們是VS Code強(qiáng)大的主要原因。由于其背后龐大的社區(qū),這些擴(kuò)展的數(shù)量變得越來越多。然而,這種龐大并不總是一件好事,因?yàn)槟闾砑拥臄U(kuò)展越多,VS Code加載的時(shí)間就越長。在某個(gè)結(jié)點(diǎn)之后,它將需要花費(fèi)超過6-7秒,如果你愿意等待那么長時(shí)間,為什么不使用IDE呢?
此外,在擴(kuò)展中,可能會(huì)出現(xiàn)一些安全性和性能問題,這些問題可能導(dǎo)致您甚至無法想象的結(jié)果。
以下是我的建議:如果擴(kuò)展對(duì)你的工作站不是那么重要,就不要安裝它。相反,看看VS Code的文檔,試著找到一種本地的方法。如我之前所說,使用設(shè)置。你可以用VS Code做很多事情。下面是一個(gè)擴(kuò)展及其設(shè)置的小列表settings.json的替代品。
雙引號(hào)彩色化
這是一個(gè)非常有用的方法,我用了很長時(shí)間。但現(xiàn)在它是在VS Code中本地實(shí)現(xiàn)的,而不是擴(kuò)展,我使用的是快速的本地?cái)U(kuò)展。
要啟用它,請(qǐng)打開settings.json,并添加以下內(nèi)容:
"editor.bracketPairColorization.enabled": true,
"editor.guides.bracketPairs":"active
自動(dòng)導(dǎo)入
自動(dòng)導(dǎo)入是另一個(gè)應(yīng)用廣泛的擴(kuò)展,高達(dá)2M+的下載文件。但當(dāng)你不需要的時(shí)候,為什么要讓你的工作空間被占用呢?
下面是VS Code開發(fā)者實(shí)現(xiàn)的相同功能。將這段代碼添加到settings.json中。
"javascript.suggest.autoImports": true,
"javascript.updateImportsOnFileMove.enabled": "always",
"typescript.suggest.autoImports": true,
"typescript.updateImportsOnFileMove.enabled": "always"
自動(dòng)關(guān)閉和重命名HTML標(biāo)簽
這些擴(kuò)展是我在系統(tǒng)上安裝的第一個(gè),但現(xiàn)在它們都沒有了,因?yàn)楝F(xiàn)在VS Code可以通過這些設(shè)置自動(dòng)做到:
"editor.linkedEditing": true,
"html.autoClosingTags": true,
"javascript.autoClosingTags": true,
"typescript.autoClosingTags": true,
Doxygen文檔生成器
這是另一個(gè)在記錄你的代碼時(shí)非常有用的擴(kuò)展,正因?yàn)槿绱?,VS code決定自己實(shí)現(xiàn)它。盡管如此,仍有600多萬用戶在自己的工作站上安裝了這個(gè)擴(kuò)展。
這是默認(rèn)啟用的,但如果不是,你可以添加以下settings.json:
"javascript.suggest.completeJSDocs": true,
"javascript.suggest.jsdoc.generateReturns": true,
"typescript.suggest.completeJSDocs": true,
"typescript.suggest.jsdoc.generateReturns": true,
更多的情況是,雖然本地就能實(shí)現(xiàn),但人們?nèi)匀皇褂猛獠繑U(kuò)展。如果你有任何建議,不要忘記在回復(fù)部分分享它們。
立即重命名
當(dāng)你不得不在整個(gè)代碼庫中更改函數(shù)或變量的名稱時(shí),因?yàn)槟悴荒苁褂煤玫膐l ' find & replace來代替它,這是很糟糕的。變量名可以在字符串中,甚至可以在另一個(gè)函數(shù)名中,改變它會(huì)破壞一切。
幸運(yùn)的是,VS Code比你想象的更聰明。它可以很容易地區(qū)分哪些字符是預(yù)期的變量名,并只更改變量名。
為此,你必須選擇需要重命名的變量并按F2。然后,輸入新的變量名并按Enter鍵。瞧!什么都沒有損壞,變量的名稱也立即改變了。
恭喜你!現(xiàn)在你知道了我用來加速開發(fā)環(huán)境的3個(gè)VS Code技巧??傊?,VS Code是一個(gè)強(qiáng)大的工具,并且實(shí)現(xiàn)了它的目的——甚至更多。然而,如果你不知道如何使用,即使你擁有世界上最好的工具也無濟(jì)于事。
END
成就一億技術(shù)人
試(Debugging)作為軟件開發(fā)環(huán)境中無法缺少的部分,長期以來都作為評(píng)價(jià)一款 IDE 產(chǎn)品優(yōu)劣的重要指標(biāo),VS Code 在 1.47 版本 中廢棄了舊版本的 Node Debug、Debugger For Chrome 等插件集,正式采用了全新的 JavaScript Debugger 插件,用于滿足所有 JavaScript 場景下的調(diào)試需求,不僅提供了豐富的調(diào)試能力,還為我們帶了了嶄新的 JavaScript Debug Terminal , Profiling 以及更好的斷點(diǎn)和源文件映射等能力。
本文將從 VSCode JavaScript Debugger 的功能入手,從源碼角度分析其實(shí)現(xiàn)對(duì)應(yīng)功能所使用的技術(shù)手段及優(yōu)秀的代碼設(shè)計(jì),讓大家對(duì)其中的功能及實(shí)現(xiàn)原理有大致理解。
同時(shí),在 2.18 版本的 OpenSumi 框架中,我們也適配了最新的 JavaScript Debugger 1.67.2 版本插件,大部分功能已經(jīng)可以正常使用,歡迎大家升級(jí)體驗(yàn)。
由于公眾號(hào)鏈接限制,文章中提到的詳細(xì)代碼均可在 https://github.com/microsoft/vscode-js-debug 倉庫中查看
VS Code JavaScript Debugger 依舊是基于 DAP 實(shí)現(xiàn)的一款 JavaScript 調(diào)試器。其支持了 Node.js, Chrome, Edge, WebView2, VS Code Extension 等研發(fā)場景調(diào)試。
DAP 是什么?
了解調(diào)試相關(guān)功能的實(shí)現(xiàn),不得不提的就是 VS Code 早期建設(shè)的 DAP (Debug Adapter Protocol)方案,其摒棄了 IDE 直接與調(diào)試器對(duì)接的方案,通過實(shí)現(xiàn) DAP 的方式,將于調(diào)試器適配的邏輯,承接在 Adapter(調(diào)試適配器) 之中,從而達(dá)到多個(gè)實(shí)現(xiàn)了同一套 DAP 協(xié)議的工具可以復(fù)用彼此調(diào)試適配器的效果,如下圖所示:
而上面圖示的適配器部分,一般組成了 VS Code 中調(diào)試插件中調(diào)試能力實(shí)現(xiàn)的核心。
目前支持 DAP 協(xié)議的開發(fā)工具列表見:Implementations Tools supporting the DAP:https://microsoft.github.io/debug-adapter-protocol/implementors/tools/ (OpenSumi 也在列表之中 ~)
多種調(diào)試能力
如上面介紹的,VS Code 中,調(diào)試相關(guān)的能力都是基于 DAP 去實(shí)現(xiàn)的,忽略建立鏈接的部分,在 JavaScript Debugger 中,所有的調(diào)試請(qǐng)求入口都在 adapter/debugAdapter.ts#L78 中處理,部分代碼如下所示:
// 初始化 Debugger
this.dap.on('initialize', params=> this._onInitialize(params));
// 設(shè)置斷點(diǎn)
this.dap.on('setBreakpoints', params=> this._onSetBreakpoints(params));
// 設(shè)置異常斷點(diǎn)
this.dap.on('setExceptionBreakpoints', params=> this.setExceptionBreakpoints(params));
// 配置初始化完成事件
this.dap.on('configurationDone', ()=> this.configurationDone());
// 請(qǐng)求資源
this.dap.on('loadedSources', ()=> this._onLoadedSources());
通過對(duì) DAP 的實(shí)現(xiàn),使得 JavaScript Debugger 可以先暫時(shí)忽略 Debug Adaptor 與不同調(diào)試器的適配邏輯,將調(diào)試抽象為一個(gè)個(gè)具體的請(qǐng)求及函數(shù)方法。
以設(shè)置斷點(diǎn)的 setBreakpoints 為例,JavaScript Debugger 將具體設(shè)置斷點(diǎn)的能力抽象與 adapter/breakpoints.ts 文件中,如下:
public async setBreakpoints(
params: Dap.SetBreakpointsParams,
ids: number[],
): Promise<Dap.SetBreakpointsResult> {
// 安裝代碼 SourceMap 文件
if (!this._sourceMapHandlerInstalled && this._thread && params.breakpoints?.length) {
await this._installSourceMapHandler(this._thread);
}
// ... 省略部分參數(shù)訂正及等待相關(guān)進(jìn)程初始化的過程
// ... 省略合并已有的斷點(diǎn)邏輯,同時(shí)移除未與調(diào)試進(jìn)程綁定的斷點(diǎn)
if (thread && result.new.length) {
// 為調(diào)試器添加斷點(diǎn)
this.ensureModuleEntryBreakpoint(thread, params.source);
// 這里的 Promise.all 結(jié)構(gòu)是為了確保設(shè)置斷點(diǎn)過程中不會(huì)因?yàn)橛脩舻哪炒?disabled 操作而丟失準(zhǔn)確性
// 相當(dāng)于取了當(dāng)前時(shí)刻有效的一份斷點(diǎn)列表
const currentList=getCurrent();
const promise=Promise.all(
result.new
.filter(this._enabledFilter)
.filter(bp=> currentList?.includes(bp))
// 實(shí)際斷點(diǎn)設(shè)置邏輯
.map(b=> b.enable(thread)),
);
// 添加斷點(diǎn)設(shè)置 promise 至 this._launchBlocker, 后續(xù)調(diào)試器依賴對(duì) `launchBlocker` 方法來確保斷點(diǎn)已經(jīng)處理完畢
this.addLaunchBlocker(Promise.race([delay(breakpointSetTimeout), promise]));
await promise;
}
// 返回?cái)帱c(diǎn)設(shè)置的 DAP 消息
const dapBreakpoints=await Promise.all(result.list.map(b=> b.toDap()));
this._breakpointsStatisticsCalculator.registerBreakpoints(dapBreakpoints);
// 更新當(dāng)前斷點(diǎn)狀態(tài)
delay(0).then(()=> result.new.forEach(bp=> bp.markSetCompleted()));
return { breakpoints: dapBreakpoints };
}
接下來可以看到 adapter/breakpoints/breakpointBase.ts#L162 中實(shí)現(xiàn)的 enable 方法,如下:
public async enable(thread: Thread): Promise<void> {
if (this.isEnabled) {
return;
}
this.isEnabled=true;
const promises: Promise<void>[]=[this._setPredicted(thread)];
const source=this._manager._sourceContainer.source(this.source);
if (!source || !(source instanceof SourceFromMap)) {
promises.push(
// 當(dāng)不存在資源或非 SourceMap 資源時(shí)
// 根據(jù)斷點(diǎn)位置、代碼偏移量計(jì)算最終斷點(diǎn)位置后在調(diào)試器文件路徑下斷點(diǎn)
this._setByPath(thread, uiToRawOffset(this.originalPosition, source?.runtimeScriptOffset)),
);
}
await Promise.all(promises);
...
}
根據(jù)資源類型進(jìn)一步處理斷點(diǎn)資源路徑,核心代碼如下(詳細(xì)代碼可見:adapter/breakpoints/breakpointBase.ts#L429):
protected async _setByPath(thread: Thread, lineColumn: LineColumn): Promise<void> {
const sourceByPath=this._manager._sourceContainer.source({ path: this.source.path });
// ... 忽略對(duì)已經(jīng)映射到本地的資源的處理
if (this.source.path) {
const urlRegexp=await this._manager._sourceContainer.sourcePathResolver.absolutePathToUrlRegexp(
this.source.path,
);
if (!urlRegexp) {
return;
}
// 通過正則表達(dá)式設(shè)置斷點(diǎn)
await this._setByUrlRegexp(thread, urlRegexp, lineColumn);
} else {
const source=this._manager._sourceContainer.source(this.source);
const url=source?.url;
if (!url) {
return;
}
// 直接通過路徑設(shè)置斷點(diǎn)
await this._setByUrl(thread, url, lineColumn);
if (this.source.path !==url && this.source.path !==undefined) {
await this._setByUrl(thread, absolutePathToFileUrl(this.source.path), lineColumn);
}
}
最終在進(jìn)程中設(shè)置斷點(diǎn)信息,部分核心代碼如下(詳細(xì)代碼可見:adapter/breakpoints/breakpointBase.ts#L513 ):
protected async _setByUrlRegexp(
thread: Thread,
urlRegex: string,
lineColumn: LineColumn,
): Promise<void> {
lineColumn=base1To0(lineColumn);
const previous=this.hasSetOnLocationByRegexp(urlRegex, lineColumn);
if (previous) {
if (previous.state===CdpReferenceState.Pending) {
await previous.done;
}
return;
}
// 設(shè)置斷點(diǎn)
return this._setAny(thread, {
urlRegex,
condition: this.getBreakCondition(),
...lineColumn,
});
}
在 node-debug/node-debug2 等插件的以往實(shí)現(xiàn)中,到這一步一般是通過向調(diào)試器發(fā)送具體 “設(shè)置斷點(diǎn)指令” 的消息,執(zhí)行相應(yīng)命令,如 node/nodeV8Protocol.ts#L463 中下面的代碼:
private send(typ: NodeV8MessageType, message: NodeV8Message) : void {
message.type=typ;
message.seq=this._sequence++;
const json=JSON.stringify(message);
const data='Content-Length: ' + Buffer.byteLength(json, 'utf8') + '\r\n\r\n' + json;
if (this._writableStream) {
this._writableStream.write(data);
}
}
而在 JavaScript Debugger 中,會(huì)將所有這類消息都抽象為統(tǒng)一的 CDP (Chrome Devtools Protocol) , 通過這種方式,抹平所有 JS 調(diào)試場景下的差異性,讓其擁有對(duì)接所有 JavaScript 場景調(diào)試場景的能力,繼續(xù)以 “設(shè)置斷點(diǎn)” 這一流程為例,此時(shí) JavaScript Debugger 不再是發(fā)送具體命令,而是通過 CDP 鏈接,發(fā)送一條設(shè)置斷點(diǎn)的消息,部分核心代碼如下(詳細(xì)代碼可見:adapter/breakpoints/breakpointBase.ts#L581 ):
const result=isSetByLocation(args)
? await thread.cdp().Debugger.setBreakpoint(args)
: await thread.cdp().Debugger.setBreakpointByUrl(args);
通過這層巧妙的 CDP 鏈接,可以將所有用戶操作指令統(tǒng)一為一層抽象的結(jié)構(gòu)處理,后面只需要根據(jù)不同的調(diào)試器類型,選擇性處理 CDP 消息即可,如圖所示:
通過這層結(jié)構(gòu)設(shè)計(jì),能讓 JavaScript Debugger 輕松兼容三種模式調(diào)試 Node Launch, Node Attach, Chrome Devtools Attach, 從而實(shí)現(xiàn)對(duì)全 JavaScript 場景的調(diào)試能力。
了解詳細(xì)的 CDP 協(xié)議,可以查看文檔 CDP (Chrome Devtools Protocol) ,在調(diào)試領(lǐng)域,Chrome Devtools 擁有更加全面的場景及能力支持,部分能力,如 DOMSnapshot 并不能在 Node 場景下使用,因此在實(shí)現(xiàn)過程中也需要選擇性處理。
同時(shí),通過這樣的改造,也讓運(yùn)行于 VS Code 中的調(diào)試進(jìn)程可以通過 Chrome Devtools 或其他支持 CDP 協(xié)議的調(diào)試工具進(jìn)行鏈接調(diào)試,如運(yùn)行 extension.js-debug.requestCDPProxy 命令獲取調(diào)試信息,如下圖所示:
在 Chrome Devtools 中可以拼接為 chrome-devtools://devtools/custom/inspector.html?ws=ws://127.0.0.1:53591/273c30144bc597afcbefa2058bfacc4b0160647e 的路徑直接進(jìn)行調(diào)試。
JavaScript Debug Terminal
如果要評(píng)選 JavaScript Debugger 中最好用的功能,那么我一定投票給 JavaScript Debug Terminal 這一功能。
JavaScript Debug Terminal 為用戶提供了一種無需關(guān)注調(diào)試配置,只需在終端運(yùn)行腳本即可快速進(jìn)行調(diào)試的能力,如下所示(OpenSumi 中的運(yùn)行效果):
眾所周知,Node.js 在調(diào)試模式下提供了兩種 flag 選項(xiàng),一個(gè)是 --inspect , 另一個(gè)則是 --inspect-brk ,兩者都可以讓 Node.js 程序以調(diào)試模式啟動(dòng),唯一區(qū)別即是 --inspect-brk 會(huì)在調(diào)試器未被 attach 前阻塞 Node.js 腳本的執(zhí)行,這個(gè)特性在老版本的 Node Debug 插件中被廣泛使用,用于保障在調(diào)試執(zhí)行前設(shè)置斷點(diǎn)等。
而在 JavaScript Debugger 中,采用了一個(gè)全新的腳本運(yùn)行模式,讓 Node.js 的調(diào)試可以不再依賴 --inspect-brk , 其原理即是向在 JavaScript Debug Terminal 中運(yùn)行的腳本注入 NODE_OPTIONS 選項(xiàng),如下所示:
在傳入 NODE_OPTIONS:'--require .../vscode-js-debug/out/src/targets/node/bootloader.bundle.js' 的環(huán)境變量后,Node.js 在腳本執(zhí)行前便會(huì)提前先去加載 bootloader.bundle.js 內(nèi)的文件內(nèi)容,而后再執(zhí)行腳本,這中間就提供了大量可操作性。
進(jìn)一步看這個(gè) targets/node/bootloader.ts#L31 文件,里面寫了一段自執(zhí)行代碼,在全局創(chuàng)建一個(gè) $jsDebugIsRegistered 對(duì)象, 通過程序內(nèi)部構(gòu)造的 VSCODE_INSPECTOR_OPTIONS 對(duì)象直接與調(diào)試進(jìn)程進(jìn)行 IPC 通信,配置格式如下所示:
{
// 調(diào)試進(jìn)程 IPC 通信地址
"inspectorIpc":"/var/folders/qh/r2tjb8vd1z3_qtlnxy47b4vh0000gn/T/node-cdp.33805-2.sock",
// 一些配置
"deferredMode":false,
"waitForDebugger":"",
"execPath":".../node",
"onlyEntrypoint":false,
"autoAttachMode":"always",
// 文件回調(diào)地址,如果存在,在調(diào)試進(jìn)程中的打印的日志將會(huì)寫入到該文件中
"fileCallback":"/var/folders/qh/r2tjb8vd1z3_qtlnxy47b4vh0000gn/T/node-debug-callback-d2db3d91a6f5ae91"
}
在獲取到 inspectorIpc 等配置后,即會(huì)嘗試通過讀文件的方式確認(rèn) inspector 進(jìn)程 的連通性,偽代碼如下(詳細(xì)代碼可見:targets/node/bootloader.ts#L246):
fs.readdirSync(path.dirname(inspectorIpc)).includes(path.basename(inspectorIpc));
在確定 inspector 進(jìn)程 的連通性后,接下來就可以使用 inspector 庫, 獲取 inspector.url() 后進(jìn)行鏈接操作,部分代碼如下(詳細(xì)代碼見:targets/node/bootloader.ts#L111):
(()=> {
...
// 當(dāng)進(jìn)程執(zhí)行時(shí)傳入了 `--inspect` 時(shí),inspector.url() 可以獲取到當(dāng)前的調(diào)試地址,命令行情況需要額外處理
const info: IAutoAttachInfo={
ipcAddress: env.inspectorIpc || '',
pid: String(process.pid),
telemetry,
scriptName: process.argv[1],
inspectorURL: inspector.url() as string,
waitForDebugger: true,
ownId,
openerId: env.openerId,
};
// 當(dāng)需要立即啟動(dòng)調(diào)試時(shí),執(zhí)行 watchdog 程序監(jiān)聽進(jìn)程創(chuàng)建
if (mode===Mode.Immediate) {
// 代碼見:https://github.com/microsoft/vscode-js-debug/blob/b056fbb86ef2e2e5aa99663ff18411c80bdac3c5/src/targets/node/bootloader.ts#L276
spawnWatchdog(env.execPath || process.execPath, info);
}
...
})();
function spawnWatchdog(execPath: string, watchdogInfo: IWatchdogInfo) {
const p=spawn(execPath, [watchdogPath], {
env: {
NODE_INSPECTOR_INFO: JSON.stringify(watchdogInfo),
NODE_SKIP_PLATFORM_CHECK: process.env.NODE_SKIP_PLATFORM_CHECK,
},
stdio: 'ignore',
detached: true,
});
p.unref();
return p;
}
接下來就是執(zhí)行下面的代碼 targets/node/watchdog.ts, 部分代碼如下:
const info: IWatchdogInfo=JSON.parse(process.env.NODE_INSPECTOR_INFO!);
(async ()=> {
process.on('exit', ()=> {
logger.info(LogTag.Runtime, 'Process exiting');
logger.dispose();
if (info.pid && !info.dynamicAttach && (!wd || wd.isTargetAlive)) {
process.kill(Number(info.pid));
}
});
const wd=await WatchDog.attach(info);
wd.onEnd(()=> process.exit());
})();
實(shí)際上這里又用了一個(gè)子進(jìn)程去處理 CDP 通信的鏈接,最終執(zhí)行到如下位置代碼 targets/node/watchdogSpawn.ts#L122,部分代碼如下:
class WatchDog {
...
// 鏈接本地 IPC 通信地址,即前面從環(huán)境變量中獲取的 inspectorIpc
public static async attach(info: IWatchdogInfo) {
const pipe: net.Socket=await new Promise((resolve, reject)=> {
const cnx: net.Socket=net.createConnection(info.ipcAddress, ()=> resolve(cnx));
cnx.on('error', reject);
});
const server=new RawPipeTransport(Logger.null, pipe);
return new WatchDog(info, server);
}
constructor(private readonly info: IWatchdogInfo, private readonly server: ITransport) {
this.listenToServer();
}
// 鏈接 Server 后,發(fā)送第一條 `Target.targetCreated` 通知調(diào)試進(jìn)程已經(jīng)可以開始調(diào)試
private listenToServer() {
const { server, targetInfo }=this;
server.send(JSON.stringify({ method: 'Target.targetCreated', params: { targetInfo } }));
server.onMessage(async ([data])=> {
// Fast-path to check if we might need to parse it:
if (
this.target &&
!data.includes(Method.AttachToTarget) &&
!data.includes(Method.DetachFromTarget)
) {
// 向 inspectorUrl 建立的鏈接發(fā)送消息
this.target.send(data);
return;
}
// 解析消息體
const result=await this.execute(data);
if (result) {
// 向調(diào)試進(jìn)程發(fā)送消息
server.send(JSON.stringify(result));
}
});
server.onEnd(()=> {
this.disposeTarget();
this.onEndEmitter.fire({ killed: this.gracefulExit, code: this.gracefulExit ? 0 : 1 });
});
}
...
}
可以看到,在 Node.js 腳本被真正執(zhí)行前,JavaScript Debug Terminal 為了讓 CDP 鏈接能夠正常初始化以及通信做了一系列工作,也正是這里的初始化操作,讓即使是在終端被執(zhí)行的腳本依舊可以與我們的調(diào)試進(jìn)程進(jìn)行 CDP 通信。
這里忽略掉了部分終端創(chuàng)建的邏輯,實(shí)際上在創(chuàng)建終端的過程中,JavaScript Debugger 也采用了一些特殊的處理,如不直接通過插件進(jìn)程創(chuàng)建終端的邏輯,而是通過 vscode.window.onDidOpenTerminal 去接收新終端的創(chuàng)建,見 ui/debugTerminalUI.ts#L197 。這些操作對(duì)于 Terminal 實(shí)例在插件進(jìn)程的唯一性有一定要求,這也是前期插件適配工作的成本之一。
Automatic Browser Debugging
看完 JavaScript Debug Terminal 的實(shí)現(xiàn)原理,我們?cè)賮砜匆幌铝硗庖粋€(gè)重要特性的實(shí)現(xiàn):Automatic browser debugging ,想要使用該功能,你需要在 JavaScript Debug Terminal 中使用,或手動(dòng)配置debug.javascript.debugByLinkOptions 為 on 或 always ,開啟了該功能后,所有你在終端以調(diào)試模式打開的網(wǎng)址將都可以自動(dòng) Attach 上響應(yīng)的調(diào)試進(jìn)程。
link-debugging.gif
其核心原理即是通過 ui/terminalLinkHandler.ts 往 Terminal 中注冊(cè)鏈接點(diǎn)擊處理邏輯,實(shí)現(xiàn) vscode.TerminalLinkProvider (https://code.visualstudio.com/api/references/vscode-api#TerminalLinkProvider) 的結(jié)構(gòu)。
export class TerminalLinkHandler implements vscode.TerminalLinkProvider<ITerminalLink>, IDisposable {
// 根據(jù)給定的 Terminal 獲取其內(nèi)容中可被點(diǎn)擊的 Link 數(shù)組,配置其基礎(chǔ)信息
public provideTerminalLinks(context: vscode.TerminalLinkContext): ITerminalLink[] {
switch (this.baseConfiguration.enabled) {
case 'off':
return [];
case 'always':
break;
case 'on':
default:
if (!this.enabledTerminals.has(context.terminal)) {
return [];
}
}
const links: ITerminalLink[]=[];
for (const link of findLink(context.line, 'url')) {
let start=-1;
while ((start=context.line.indexOf(link.value, start + 1)) !==-1) {
let uri: URL;
try {
uri=new URL(link.href);
} catch {
continue;
}
// hack for https://github.com/Soapbox/linkifyjs/issues/317
if (
uri.protocol===Protocol.Http &&
!link.value.startsWith(Protocol.Http) &&
!isLoopbackIp(uri.hostname)
) {
uri.protocol=Protocol.Https;
}
if (uri.protocol !==Protocol.Http && uri.protocol !==Protocol.Https) {
continue;
}
links.push({
startIndex: start,
length: link.value.length,
tooltip: localize('terminalLinkHover.debug', 'Debug URL'),
target: uri,
workspaceFolder: getCwd()?.index,
});
}
}
return links;
}
/**
* 處理具體點(diǎn)擊鏈接后的操作
*/
public async handleTerminalLink(terminal: ITerminalLink): Promise<void> {
if (!(await this.handleTerminalLinkInner(terminal))) {
vscode.env.openExternal(vscode.Uri.parse(terminal.target.toString()));
}
}
}
在鏈接被打開前,會(huì)進(jìn)入 handleTerminalLinkInner 的邏輯進(jìn)行調(diào)試進(jìn)程的鏈接處理,如下:
向上檢索默認(rèn)的瀏覽器信息,是否為 Edge,否則使用 pwa-chrome 調(diào)試類型啟動(dòng)調(diào)試。
在找不到對(duì)應(yīng)調(diào)試信息(即 DAP 消息)的情況下,輸出 Using the "preview" debug extension , 結(jié)束調(diào)試。
Profile
Profile 主要為開發(fā)者提供對(duì)進(jìn)程性能及堆棧信息的分析能力,在 JavaScript Debugger 中,由于所有的通信均通過 CDP 協(xié)議處理,生成的報(bào)告文件也自然的能通過 Chrome Devtools 中查看,VS Code 中默認(rèn)僅支持基礎(chǔ)的報(bào)告查看,你也可以通過安裝 ms-vscode.vscode-js-profile-flame 插件查看。
實(shí)現(xiàn)該功能依舊是通過 DAP 消息進(jìn)行通信處理,與上面提到的 設(shè)置斷點(diǎn) 案例實(shí)際類似,DAP 通信中收到 startSefProfile 時(shí)開始向 CDP 鏈接發(fā)送 Profiler.enable ,Profiler.start 指令,進(jìn)而在不同的調(diào)試器中處理該指令,在 DAP 通信中收到 stopSelfProfile 指令時(shí),向 CDP 鏈接發(fā)送 Profiler.stop 指令,收集 profile 信息后寫入對(duì)應(yīng)文件,詳細(xì)代碼可見:adapter/selfProfile.ts
Debug Console
JavaScript Debugger 在發(fā)布日志中著重標(biāo)注了對(duì)于 Top-Level await 的支持,原有的 DebugConsole 對(duì)于變量執(zhí)行的邏輯依舊是依賴 DAP 中接收 evaluate 指令(代碼見:adapter/debugAdapter.ts#L99) ,繼而轉(zhuǎn)化為 CDP 的 Runtime.evaluate 指令執(zhí)行。由于不同調(diào)試器運(yùn)行環(huán)境的差異性,變量或表達(dá)式最終的執(zhí)行指令需要根據(jù)環(huán)境進(jìn)行區(qū)分處理(詳細(xì)代碼可見:adapter/threads.ts#L394)。以在調(diào)試控制臺(tái)執(zhí)行如下代碼為例:
const res=await fetch('http://api.github.com/orgs/microsoft');
console.log(await res.json());
當(dāng)表達(dá)式為 Top-Level await 時(shí),需要將表達(dá)式進(jìn)行重寫
從上面的表達(dá)式轉(zhuǎn)化為可執(zhí)行的閉包結(jié)構(gòu)(解析邏輯可見:common/sourceUtils.ts#L67)同時(shí)在參數(shù)中標(biāo)記 awaitPromise=true , 在部分調(diào)試器執(zhí)行 Runtime.evalute 時(shí),當(dāng)參數(shù)中存在 awaitPromise=true 時(shí),會(huì)將閉包執(zhí)行的返回結(jié)果作為輸出值進(jìn)行返回,轉(zhuǎn)化后的結(jié)果如下所示:
(async ()=> {
(res=await fetch('http://api.github.com/orgs/microsoft'));
return console.log(await res.json());
})();
最終執(zhí)行結(jié)果就能夠正常輸出:
這樣便實(shí)現(xiàn)了對(duì) Top-Level await 的支持。
以上整體上是針對(duì)部分功能實(shí)現(xiàn)的解析,部分功能的優(yōu)化也依賴 DAP 及代碼邏輯的優(yōu)化實(shí)現(xiàn),如更好的代碼映射及 Return value interception 等,希望看完本文能讓你對(duì) VS Code 的 JavaScript Debugger 有大致的理解。
者:biaochenxuying
轉(zhuǎn)發(fā)鏈接:https://github.com/biaochenxuying/blog/issues/31
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。