常见 Bug 调试技巧
插件出问题时,报错往往只有一行。本文从"崩溃日志在哪里"讲起,到"如何用开发者工具解剖 Webview"、"如何隔离嫌疑扩展",再到"如何用分级日志把问题提前暴露",组成一套从定位到修复的完整排障链路。
扩展宿主崩溃日志分析
扩展宿主(Extension Host)是插件运行所在的进程。它崩溃时,错误记录在 VS Code 的日志目录里。
| 平台 | 日志目录 |
|---|---|
| Windows | %APPDATA%\Code\logs\<时间戳>\ |
| macOS | ~/Library/Application Support/Code/logs/<时间戳>/ |
| Linux | ~/.config/Code/logs/<时间戳>/ |
日志目录下有多个子文件,各司其职:
| 文件 | 内容 |
|---|---|
exthost/exthost.log | 扩展宿主日志:插件加载、激活、崩溃、IPC 错误 |
main.log | 主进程日志:窗口、任务、更新 |
renderer.log | 渲染进程日志:UI、Webview 相关 |
sharedprocess.log | 共享进程日志:状态服务、文件监听 |
不用手动去翻目录——命令面板执行 Developer: Open Logs Folder 直接打开当前会话的日志目录。
扩展宿主崩溃的典型日志:
[exthost] [error] Extension host terminated unexpectedly.
[exthost] [error] Type: RangeError
[exthost] [error] Message: Maximum call stack size exceeded
[exthost] [error] Stack: at Object.getText ...--log 参数控制日志详细程度,按顺序从静到详依次是 error、warn、info、debug、trace。F5 调试时在 launch.json 中传参:
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--log=debug",
"--logExtensionHost=true"
]
}--log=debug 把扩展宿主日志提到 debug 级,--logExtensionHost=true 把扩展宿主日志转发到调试控制台,崩溃前的最后几行调用往往就是根因线索。
Toggle Developer Tools 调试 Webview
Webview 是插件里的独立网页,普通断点调试不到它内部。命令面板执行 Developer: Toggle Developer Tools,会弹出 Chrome DevTools 面板——Webview 本质上就是一个 iframe 页面,DevTools 的三个面板各有用途:
| 面板 | 排查什么 |
|---|---|
| Console | JS 报错、未捕获异常、console.log 输出 |
| Network | 资源加载失败(404)、CSP 拦截的资源、加载耗时 |
| Elements | DOM 结构与样式、消息通信的占位节点 |
DevTools 面板切换到 Webview 的两种方式:
1. 点击面板左上角的 iframe 下拉框,选择你的 Webview 标题
2. 在 Console 里输入 document.querySelectorAll('iframe')Console 里常见的 Webview 报错及含义:
Refused to execute inline script because it violates CSP
# Webview 默认 CSP 禁内联脚本,改用外部 js 文件或用 nonce/hash
Not allowed to load local resource: file:///xxx
# 本地资源必须经 asWebviewUri 转换后才能加载Network 面板若看到资源标红,基本是路径拼错或 asWebviewUri 漏用。Elements 面板可临时修改样式定位布局问题,但改完要回源码同步——DevTools 里的修改不持久。
Reload Window With Extensions Disabled 隔离问题
症状"装了某插件后 VS Code 变卡 / 报错",但不知道是谁干的。命令面板执行 Developer: Reload Window With Extensions Disabled,窗口以"禁用全部插件"模式重载——先确认问题是否还出现:
重载后问题消失 → 是插件引起,进入二分排查
重载后问题依旧 → 与插件无关,排查设置/工作区/VS Code 本身确定是插件问题后,二分法定位:命令面板执行 Extensions: Disable All Installed Extensions 后逐个启用,或用 Developer: Show Running Extensions 查看每个插件的激活耗时与内存占用,找出异常者。
针对自己正在开发的插件,快速判断"是不是它":
1. Developer: Reload Window With Extensions Disabled —— 确认基准状态
2. 在 launch.json 里去掉 --extensionDevelopmentPath,重新 F5 —— 你的插件没加载
3. 比较两种状态下问题是否出现,即可断定责任在不在插件代码Output Channel 日志分级输出
把日志写进 VS Code 的输出面板(Output Channel),比 console.log 专业得多:不污染调试控制台、用户可随时查看、可分级过滤。先做一个分级输出工具:
import * as vscode from 'vscode';
export enum LogLevel {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3
}
export class Logger {
private channel: vscode.OutputChannel;
private level: LogLevel = LogLevel.Info;
constructor(name: string) {
// 输出面板中以独立频道显示
this.channel = vscode.window.createOutputChannel(name);
// debug 开关:配置项控制是否输出 debug 级日志
const cfg = vscode.workspace.getConfiguration('myext');
const debug = cfg.get<boolean>('debug', false);
this.level = debug ? LogLevel.Debug : LogLevel.Info;
}
debug(msg: string, ...args: unknown[]) {
this.log(LogLevel.Debug, '[debug]', msg, args);
}
info(msg: string, ...args: unknown[]) {
this.log(LogLevel.Info, '[info]', msg, args);
}
warn(msg: string, ...args: unknown[]) {
this.log(LogLevel.Warn, '[warn]', msg, args);
}
error(msg: string, ...args: unknown[]) {
this.log(LogLevel.Error, '[error]', msg, args);
}
private log(level: LogLevel, tag: string, msg: string, args: unknown[]) {
// 低于当前级别的日志直接丢弃
if (level < this.level) return;
const time = new Date().toISOString().replace('T', ' ').slice(0, 19);
const detail = args.length ? ' ' + args.map(JSON.stringify).join(' ') : '';
this.channel.appendLine(`${time} ${tag} ${msg}${detail}`);
}
// 每次会话开始输出分隔线,便于区分多次运行
show() {
this.channel.show();
}
dispose() {
this.channel.dispose();
}
}配合配置项,用户可自行打开详细日志:
{
"contributes": {
"configuration": {
"properties": {
"myext.debug": {
"type": "boolean",
"default": false,
"description": "是否输出 debug 级日志"
}
}
}
}
}分级的原则:error 记录不该发生的异常路径,warn 记录可恢复的异常情况,info 记录关键操作(激活、命令触发、配置变更),debug 记录细粒度调用过程。排查时把 myext.debug 打开,关键操作的前后文一目了然。
// 使用示例
const logger = new Logger('MyExt');
export function activate(context: vscode.ExtensionContext) {
logger.info('插件激活');
logger.debug('配置信息', vscode.workspace.getConfiguration('myext'));
context.subscriptions.push(
vscode.commands.registerCommand('myext.doSomething', async () => {
logger.info('doSomething 命令触发');
try {
// 业务逻辑
logger.debug('处理完成');
} catch (err) {
// 异常路径必记 error,附带堆栈
logger.error('doSomething 失败', err);
}
})
);
}诊断报错快速定位
VS Code 有自己的一套"诊断"体系:问题面板(Problem Panel)、错误提示、状态栏报错。插件抛出未捕获异常时,界面上的报错提示往往指向插件名与行号,但信息有限。快速定位的三板斧:
| 手法 | 操作 | 定位什么 |
|---|---|---|
| 打开日志目录 | Developer: Open Logs Folder | 崩溃栈、进程异常 |
| 看扩展宿主日志 | exthost.log 搜插件 ID | 插件内 console.error 与未捕获异常 |
| 开发者工具 | Developer: Toggle Developer Tools | 渲染层错误、CSP、资源加载 |
未捕获异常的统一兜底,让错误信息带上下文地落进日志:
export function activate(context: vscode.ExtensionContext) {
// 全局捕获扩展宿主的未处理异常(含 async 未 await 的 Promise 拒绝)
process.on('uncaughtException', (err) => {
logger.error('未捕获异常', err);
});
process.on('unhandledRejection', (reason) => {
logger.error('未处理的 Promise 拒绝', reason);
});
}再配合 vscode.window.showErrorMessage 把关键错误直接弹给用户:
try {
await riskyOperation();
} catch (err) {
// 弹窗给用户 + 日志留痕,双通道
const msg = err instanceof Error ? err.message : String(err);
vscode.window.showErrorMessage(`操作失败:${msg}`);
logger.error('riskyOperation 失败', err);
}一个容易忽视的点:插件里写 console.log 默认只在开发模式(F5)可见,用户环境看不到。线上问题的第一现场永远是 Output Channel 与日志文件——所以从第一天就用分级 Logger,而不是 console.log。崩溃日志、DevTools、扩展隔离、分级日志四件套齐备,绝大多数插件 Bug 都能在几分钟内定位到根因。