关于 vscode-extension-expert
为 VS Code 扩展开发者提供参考指南,涵盖 API 用法、架构设计、WebView 与 LSP 实现、安全、性能、测试和发布实践。
主要功能
- 01VS Code Extension API 指南
- 02架构与激活模式
- 03WebView 安全实践
- 04LSP 实现指南
- 05性能与测试建议
- 06Marketplace 发布检查清单
适用场景
- 01规划新的 VS Code 扩展
- 02添加 WebView 或语言功能
- 03排查激活和运行时问题
- 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 开发
安全要求(重要)
- 内容安全策略(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>`;
}
- 输入净化——始终净化用户输入
- 仅使用 HTTPS——外部资源必须使用 HTTPS
- 最小权限——限制
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