mcpField
V
vscode-extension-expert

vscode-extension-expert

github.com/s-hiraoku开发22160 次安装8 周前更新
代码仓库

关于 vscode-extension-expert

为 VS Code 扩展开发者提供参考指南,涵盖 API 用法、架构设计、WebView 与 LSP 实现、安全、性能、测试和发布实践。

主要功能

  1. 01VS Code Extension API 指南
  2. 02架构与激活模式
  3. 03WebView 安全实践
  4. 04LSP 实现指南
  5. 05性能与测试建议
  6. 06Marketplace 发布检查清单

适用场景

  1. 01规划新的 VS Code 扩展
  2. 02添加 WebView 或语言功能
  3. 03排查激活和运行时问题
  4. 04准备发布扩展

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/s-hiraoku/vscode-sidebar-terminal --skill vscode-extension-expert

安装后在 Claude Code 中输入/vscode-extension-expert确认已加载。

详细介绍

VS Code 扩展专家

概述

此技能通过提供关于 VS Code 扩展 API、架构模式、安全要求和最佳实践的全面知识,帮助实现专家级的 VS Code 扩展开发。创建新扩展、为现有扩展添加功能、实现 WebView、设计语言支持或优化性能时,都应使用此技能。

使用时机

  • 实现新的 VS Code 扩展功能
  • 设计扩展架构和结构
  • 创建安全性得当的基于 WebView 的 UI
  • 实现语言服务器协议(LSP)功能
  • 调试扩展激活或运行时问题
  • 优化扩展性能和启动时间
  • 准备将扩展发布到 Marketplace

核心概念

扩展组成

每个 VS Code 扩展都需要:

extension-name/
├── .vscode/              # Debug configurations
│   ├── launch.json
│   └── tasks.json
├── src/
│   └── extension.ts      # Main entry point
├── package.json          # Extension manifest (critical)
├── tsconfig.json         # TypeScript config
└── .vscodeignore         # Exclude from package
package.json 必要字段
{
  "name": "extension-name",
  "publisher": "publisher-id",
  "version": "0.0.1",
  "engines": { "vscode": "^1.80.0" },
  "main": "./out/extension.js",
  "activationEvents": [],
  "contributes": {
    "commands": [],
    "configuration": {},
    "views": {}
  },
  "extensionKind": ["workspace"]
}
扩展入口点模式
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  // Register commands, providers, listeners
  const disposable = vscode.commands.registerCommand('ext.command', () => {
    // Command implementation
  });

  context.subscriptions.push(disposable);
}

export function deactivate() {
  // Cleanup resources
}

激活事件

选择最具体的激活事件,以尽量减少对启动的影响:

事件使用场景示例
onLanguage:<lang>特定语言的功能onLanguage:python
onCommand:<command>由命令驱动的扩展onCommand:ext.showPanel
onView:<viewId>展开侧边栏视图onView:myTreeView
workspaceContains:<glob>特定项目的功能workspaceContains:**/.eslintrc*
onFileSystem:<scheme>自定义文件系统onFileSystem:sftp
onStartupFinished后台任务(优先于 *)

重要:避免使用 *,因为它会在每次 VS Code 启动时激活。

贡献点

命令
{
  "contributes": {
    "commands": [{
      "command": "ext.doSomething",
      "title": "Do Something",
      "category": "My Extension",
      "icon": "$(symbol-method)"
    }]
  }
}
配置
{
  "contributes": {
    "configuration": {
      "title": "My Extension",
      "properties": {
        "myExtension.enabled": {
          "type": "boolean",
          "default": true,
          "description": "Enable the extension"
        }
      }
    }
  }
}
视图(树视图)
{
  "contributes": {
    "views": {
      "explorer": [{
        "id": "myTreeView",
        "name": "My View"
      }]
    },
    "viewsContainers": {
      "activitybar": [{
        "id": "myContainer",
        "title": "My Extension",
        "icon": "resources/icon.svg"
      }]
    }
  }
}

VS Code API 命名空间

window API
// Show messages
vscode.window.showInformationMessage('Hello!');
vscode.window.showErrorMessage('Error occurred');

// Quick picks
const item = await vscode.window.showQuickPick(['Option 1', 'Option 2']);

// Input boxes
const input = await vscode.window.showInputBox({ prompt: 'Enter value' });

// Active editor
const editor = vscode.window.activeTextEditor;
workspace API
// Read configuration
const config = vscode.workspace.getConfiguration('myExtension');
const value = config.get<boolean>('enabled');

// Watch files
const watcher = vscode.workspace.createFileSystemWatcher('**/*.ts');
watcher.onDidChange(uri => { /* handle change */ });

// Open documents
const doc = await vscode.workspace.openTextDocument(uri);
commands API
// Register
const disposable = vscode.commands.registerCommand('ext.cmd', (arg) => {
  // Implementation
});

// Execute
await vscode.commands.executeCommand('ext.cmd', argument);

WebView 开发

安全要求(重要)
  1. 内容安全策略(CSP)——始终实施严格的 CSP:
function getWebviewContent(webview: vscode.Webview): string {
  const nonce = getNonce();

  return `<!DOCTYPE html>
  <html>
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="Content-Security-Policy" content="
      default-src 'none';
      style-src ${webview.cspSource} 'unsafe-inline';
      script-src 'nonce-${nonce}';
      img-src ${webview.cspSource} https:;
    ">
  </head>
  <body>
    <script nonce="${nonce}">
      const vscode = acquireVsCodeApi();
      // Use vscode.postMessage() for communication
    </script>
  </body>
  </html>`;
}
  1. 输入净化——始终净化用户输入
  2. 仅使用 HTTPS——外部资源必须使用 HTTPS
  3. 最小权限——限制 localResourceRoots
消息传递模式
// Extension → WebView
panel.webview.postMessage({ type: 'update', data: payload });

// WebView → Extension
panel.webview.onDidReceiveMessage(message => {
  switch (message.type) {
    case 'action':
      handleAction(message.data);
      break;
  }
});

// In WebView JavaScript
window.addEventListener('message', event => {
  const message = event.data;
  // Handle message
});

vscode.postMessage({ type: 'action', data: result });
状态持久化
// Simple state (survives webview hide/show)
const state = webview.getState() || { count: 0 };
webview.setState({ count: state.count + 1 });

// Full persistence (survives VS Code restart)
class MySerializer implements vscode.WebviewPanelSerializer {
  async deserializeWebviewPanel(panel: vscode.WebviewPanel, state: any) {
    panel.webview.html = getHtmlForWebview(panel.webview, state);
  }
}

vscode.window.registerWebviewPanelSerializer('myWebview', new MySerializer());

语言服务器协议(LSP)

架构
┌─────────────────────┐     ┌─────────────────────┐
│  Language Client    │────│  Language Server    │
│  (VS Code Extension)│ LSP │  (Separate Process) │
│  vscode-languageclient    │  vscode-languageserver
└─────────────────────┘     └─────────────────────┘
客户端实现
import { LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node';

const serverOptions: ServerOptions = {
  run: { module: serverPath, transport: TransportKind.ipc },
  debug: { module: serverPath, transport: TransportKind.ipc }
};

const clientOptions: LanguageClientOptions = {
  documentSelector: [{ scheme: 'file', language: 'mylang' }],
  synchronize: {
    fileEvents: vscode.workspace.createFileSystemWatcher('**/*.mylang')
  }
};

const client = new LanguageClient('mylang', 'My Language', serverOptions, clientOptions);
client.start();
服务端实现
import { createConnection, TextDocuments, ProposedFeatures } from 'vscode-languageserver/node';
import { TextDocument } from 'vscode-languageserver-textdocument';

const connection = createConnection(ProposedFeatures.all);
const documents = new TextDocuments(TextDocument);

connection.onInitialize((params) => {
  return {
    capabilities: {
      textDocumentSync: TextDocumentSyncKind.Incremental,
      completionProvider: { resolveProvider: true },
      hoverProvider: true
    }
  };
});

connection.onCompletion((params) => {
  return [
    { label: 'suggestion1', kind: CompletionItemKind.Text }
  ];
});

documents.listen(connection);
connection.listen();

树视图实现

class MyTreeDataProvider implements vscode.TreeDataProvider<MyItem> {
  private _onDidChangeTreeData = new vscode.EventEmitter<MyItem | undefined>();
  readonly onDidChangeTreeData = this._onDidChangeTreeData.event;

  refresh(): void {
    this._onDidChangeTreeData.fire(undefined);
  }

  getTreeItem(element: MyItem): vscode.TreeItem {
    return {
      label: element.name,
      collapsibleState: element.children ?
        vscode.TreeItemCollapsibleState.Collapsed :
        vscode.TreeItemCollapsibleState.None,
      command: {
        command: 'ext.selectItem',
        title: 'Select',
        arguments: [element]
      }
    };
  }

  getChildren(element?: MyItem): Thenable<MyItem[]> {
    if (!element) {
      return Promise.resolve(this.getRootItems());
    }
    return Promise.resolve(element.children || []);
  }
}

// Register
const provider = new MyTreeDataProvider();
vscode.window.registerTreeDataProvider('myTreeView', provider);

性能最佳实践

延迟加载
// Delay expensive imports
let heavyModule: typeof import('./heavyModule') | undefined;

async function getHeavyModule() {
  if (!heavyModule) {
    heavyModule = await import('./heavyModule');
  }
  return heavyModule;
}
打包(VS Code Web 必需)

使用 esbuild 快速打包:

// esbuild.config.js
const esbuild = require('esbuild');

esbuild.build({
  entryPoints: ['./src/extension.ts'],
  bundle: true,
  outfile: './out/extension.js',
  external: ['vscode'],
  format: 'cjs',
  platform: 'node',
  minify: process.env.NODE_ENV === 'production',
  sourcemap: true
});
资源清理
export function activate(context: vscode.ExtensionContext) {
  // Always add to subscriptions for automatic cleanup
  context.subscriptions.push(
    vscode.commands.registerCommand(...),
    vscode.window.registerTreeDataProvider(...),
    watcher,
    client
  );
}

export function deactivate() {
  // Explicit cleanup for async resources
  return client?.stop();
}

测试策略

使用 @vscode/test-cli 进行集成测试
// .vscode-test.js
const { defineConfig } = require('@vscode/test-cli');

module.exports = defineConfig({
  files: 'out/test/**/*.test.js',
  version: 'stable',
  workspaceFolder: './test-fixtures',
  mocha: {
    timeout: 20000  // Note: @vscode/test-cli uses Mocha for VS Code extension host tests
  }
});
测试结构
import * as assert from 'assert';
import * as vscode from 'vscode';

suite('Extension Test Suite', () => {
  vscode.window.showInformationMessage('Start tests.');

  test('Command registration', async () => {
    const commands = await vscode.commands.getCommands();
    assert.ok(commands.includes('ext.myCommand'));
  });

  test('Configuration access', () => {
    const config = vscode.workspace.getConfiguration('myExtension');
    assert.strictEqual(config.get('enabled'), true);
  });
});

常见问题及解决方案

扩展未激活

原因:激活事件与用户操作不匹配
解决方案:确认 package.json 中的 activationEvents 与实际触发条件一致

WebView 安全错误

原因:缺少 CSP 或 CSP 不正确
解决方案:始终包含严格的 Content-Security-Policy meta 标签

内存泄漏

原因:事件侦听器或可释放对象未跟踪
解决方案:将所有可释放对象添加到 context.subscriptions

启动缓慢

原因:activate() 中存在耗时的同步操作
解决方案:使用延迟加载,并推迟非关键初始化

命令未显示在命令面板中

原因:缺少 contributes.commands 声明
解决方案:确保命令已在 package.json 中声明,并通过 registerCommand 注册

安全检查清单

  • 为 WebView 实施严格的内容安全策略
  • 在渲染前净化所有用户输入
  • 对外部资源使用 HTTPS
  • 验证来自 WebView 的所有消息
  • 将 localResourceRoots 限制为必要路径
  • 使用带有单词边界的正则表达式验证 URL(不要使用 includes())
  • 不要在设置中存储机密(使用 SecretStorage)

发布检查清单

  • 名称和发布者组合具有唯一性
  • PNG 图标(最低 128x128)
  • 完整的 README.md,包含功能和屏幕截图
  • 包含版本历史记录的 CHANGELOG.md
  • LICENSE 文件
  • 语义化版本控制
  • .vscodeignore,用于排除开发文件
  • 在 Windows、macOS 和 Linux 上进行测试
  • 如有需要,为兼容 Web 构建程序包

资源

如需详细的参考文档,请参阅:

  • references/api-reference.md - 完整的 VS Code API 文档
  • references/webview-security.md - WebView 安全指南
  • references/lsp-guide.md - Language Server Protocol 实现指南

如需工作示例,请参阅官方示例:

评论

支持 Markdown