这并不是一篇 VScode 插件 API 字典,因为这是 code 官方文档 的职责。本文旨在让你了解我们在开发 VScode 插件时的思考过程,帮助你掌握查阅文档的能力。
换句话说,本文的核心目的是帮助读者理清,开发 VScode 插件时我们会想些什么。
TIPS:
本文章在 “开发” 章节之前主要输出一些关键的点,不涉及完整的开发细节。 如果你想直接了解怎么写一个插件,可以从 “开发” 开始看,如果你想先对 vscode 插件有一个相对完整的认知,再了解具体怎么写,请顺序阅读。
1. VScode 基本结构
在正式进入 VScode 插件开发之前,我们先简单了解一下 VScode 本身。
1.1 基于 Electron 的 VScode(架构)
VScode 是基于 electron 构建的项目。其宏观上的运行结构如下图:
-
Chromium: Blink(渲染) + V8(前端脚本,Web API)
-
Node.js: 操作系统级别能力(文件读写,进程管理,网络管理)
-
Native API: 特定平台的操作系统交互能力(对话框,窗口事件...)
1.2 进程结构(运行)
Electron 程序是多进程的。
-
主进程 Main Process: 应用启动第一个进程是主进程,负责原生 GUI, 窗口管理,控制整个应用的生命周期,访问底层操作系统 API
-
渲染进程 Renderer Process: 被主进程管理,每个窗口通常对应一个渲染进程,负责前端渲染,页面脚本执行。通过 IPC 与主进程通信。
-
GPU 进程 (可选): GPU 加速任务
在 VScode 这个应用的场景中:
其结构示意图如下:
-
主进程:整个应用的生命周期,处理菜单等全局UI,快捷键
-
渲染进程:VSCode 主窗口的渲染,处理用户交互,文本编辑,文件树视图
-
拓展主机进程 Extension Host:隔离运行插件(可以有多个该进程,用于不同的拓展插件)
-
LSP 进程: 关于 LSP 又是一个庞大的内容,它抽象了对语言的支持,比如代码补全,快速跳转,语法错误诊断...
-
....
VScode 运行时,活动监控截图:
VScode 插件运行在单独的进程中。
这样做明显的好处是提高了安全性(因为插件的来源是非信任的,假设插件崩了,VScode 本身应该保持稳定)。
当插件激活时,新的插件进程启动,执行插件逻辑。
开屏动画播完了,现在,我们正式开始学习插件开发:
从宏观上看,学习插件开发的两个关键是:
- 边界: 所谓边界,就是我们的插件能增强 VScode 一些什么?(显然取决于 VScode 提供的 api 的边界)
- 规范: 所谓规范,就是我们如何告诉 VScode 我们要干嘛?( 不妨想想如何你是设计者,你会如何做?常见的就是特定格式的配置文件解析 + 回调函数注册 )
我们围绕上面两个两个问题展开。
2. 边界
2.1 VScode 本身是什么?
从产品的角度来看,VScode 作为一款代码编辑器,所有的功能点无非是围绕着开发体验。
编辑器(Editor)是根,其他 UI 用来辅助开发。
2.2 VScode 插件能做些什么
在探讨如何编写 VScode 插件之前,先来考虑插件可能有哪些能力,即他能拓展 VScode 什么?
-
语言支持: 代码补全提示,格式化,跳转,代码检查...
-
主题配色:语法高亮,主题配置
-
编辑器增强: 扩充编辑器的功能,举例:Auto Import(自动导入引用),Image preview(预览导入的图片)...
-
VScode 通用功能: 快捷键,命令,消息通知...
-
UI 扩充: 侧边栏,底部状态栏,Panel...(见上图)
-
...
但总的来看,插件主要可以做两件事:
-
增强代码编辑体验(围绕文本编辑)
-
扩充 UI(嵌入一些除了编辑器以外的其他UI)
3. 规范
先介绍后实践,此处只是大致描述让大家产生一些必要的概念认识,详细的实践在后面 “开发” 一章中
VScode 本身是一个庞大的程序。如何拓展一个已有的程序?根据我们的经验也可以想到:
-
编辑配置文件(基本信息)
-
注册回调函数(逻辑)
3.1 配置文件(基本信息)
VScode 插件的配置文件就是项目根目录下的 package.json 文件
配置文件能声明哪些信息?
-
基本信息(插件名,作者,版本,描述,图标,分类,标签)
- name, version, publisher, description, author, icon ...
-
引擎和依赖(引擎负责标识插件适用的 VScode 版本,依赖则是该项目的依赖文件)
-
engines, dependencies, devDependencies ...
-
很多功能都是见名知意,举个🌰:
-
{ "name": "demo", "displayName": "一个很强大的插件", "version": "0.1.0", "publisher": "cosine", "description": "这个插件可以用来实现你的三个愿望", "author": { "name": "cosine" }, "categories": ["Other"],//vscode问你,该插件是干啥的,主题?语言支持?或者其他 "icon": "images/icon.png", "engines": { "vscode": "^1.0.0" }, "main": "./out/extension", "scripts": { "vscode:prepublish": "node ./node_modules/vscode/bin/compile", "compile": "node ./node_modules/vscode/bin/compile -watch -p ./" }, "devDependencies": { "@types/vscode": "^0.10.x", "typescript": "^1.6.2" }, "license": "SEE LICENSE IN LICENSE.txt", "bugs": { "url": "https://github.com/microsoft/vscode-wordcount/issues", "email": "sean@contoso.com" }, "repository": { "type": "git", "url": "https://github.com/microsoft/vscode-wordcount.git" }, "homepage": "https://github.com/microsoft/vscode-wordcount/blob/main/README.md" }
-
-
激活事件
- 该字段告诉 VScode 该插件什么时候激活
- 为什么需要设置激活事件?我们的插件应该“按需”加载,节省资源,所谓的需就是通过这个字段配置
-
{ //其他配置, "activationEvents": [ "onLanguage:json", "onLanguage:markdown", "onLanguage:typescript" ] }
举个🌰,这段配置表示当打开 json , md ,ts 类型文件时,插件激活
-
入口文件
main(指定了插件脚本的入口文件,见下文) -
贡献点
contributes(即拓展了什么功能) (划重点)- 如下图,是 pitaya 插件在商店中展示的截图,
功能贡献中展示了该插件的激活事件,贡献点。 - 这俩信息说明了插件在什么情况下会被激活(打开特定类型的文件?还是点了插件图标,还是通过快捷键,命令...),以及该插件都拓展了什么(拓展UI?增强语言?)
- 如下图,是 pitaya 插件在商店中展示的截图,
这里掏出几种带大家感受:
新增一个 Command:
{
// 其他配置...
"contributes": {
// TIPS:
// 此处只是告诉 VScode 我有这么一个 command。
// 至于这个 command 触发后会发生什么?需要通过下文的回调函数声明
"commands": [
{
"command": "extension.sayHello",
"title": "Hello World",
"category": "Hello",
}
]
}
}
新增一个 MenuItem
{
// 其他配置
"contributes": {
"menus": {
"view/title": [
{
"command": "terminalApi.sendText",
"when": "view == terminal",
"group": "navigation"
}
]
}
}
}
总而言之, contributes 字段是整个配置文件的核心,是告诉 VScode 我这个插件都想干啥。
小提醒,这里只是告诉了 VScode 我们想做什么?而具体怎么做的,是通过 main 字段指定的脚本实现的
关于更加完整详细,请看 VCR:
package.json 配置详情:
code.visualstudio.com/api/referen…
Contributes 字段配置详情:
code.visualstudio.com/api/referen…
激活事件配置详情:
code.visualstudio.com/api/referen…
3.2 回调函数(插件逻辑)
前文提到,可以在 package.json 中的 main 字段可声明入口文件地址,如:
{
//...其他字段
"main": "./dist/extension.js",
}
小学二年级学过,回调函数的关键有俩,1. 运行时机 2. 调用者传入的参数。
3.2.1 运行时机
VScode 会根据 main 字段读取该文件夹下的两个函数:
-
入口函数
reactive****:在插件激活时调用,用来声明插件本身的逻辑 -
出口函数
deactivate:在插件销毁时调用,用来清理一些内存资源
🌰 举个例子:
// 激活时注册插件逻辑:
export function activate(context: ExtensionContext) {
// ... 一些业务逻辑:
Logger.showDefaultLog();
const provider = registerWebviewView(context);
registerCommands(context, provider);
}
// 插件销毁时调用,无参数
export function deactivate() {
// ...释放插件中的一些资源
Logger.dispose();
}
3.2.2 VScode API(划重点)
在插件中,你需要使用的 API 主要来自两个地方
- 导入 vscode
import * as vscode from 'vscode': vscode 环境交互通用功能 - 回调参数
context: 当前拓展实例的一些信息
我们统称为 VScode API
关于 context:
activate 函数接收参数 context: VScode.ExtensionContext (附上源码链接)
学习 VScode 插件开发的本质就是熟悉 VScode API 提供了哪些能力。
因为 VScode 插件能干啥,取决于 VScode API 提供的能力。
作为 VScode API 的用户,不妨先想想自己可能有哪些需求需要被 VScode API 满足:
-
命令的注册(当命令触发时我要干嘛)
-
UI 的注册(为了扩充 UI,我们应该有能力告诉 VScode 在哪个位置显示什么)
-
文件操作能力(读写用户 Workspace 中的文件)
-
Workspace 配置的读写(即修改和读取用户的 VScode 配置)
-
Editor 内容的读写(比如检测当前光标的文本,单词,格式化文本的能力)
-
....
是的,vscode API 都会满足你。下面举一些简单的🌰
-
注册一个命令:
import * as vscode from "vscode";
export function activate(context: vscode.ExtensionContext) {
let disposable = vscode.commands.registerCommand(
//在 package.json contributions 字段中注册的 Command ID
"myExtension.helloWorld",
// command 触发时的回调函数
() => {
vscode.window.showInformationMessage("Hello World from My Extension!");
}
);
context.subscriptions.push(disposable);
}
export function deactivate() {}
-
context.subscriptions
值得一提的是,context.subscriptions 是一个用来释放资源的栈,vscode 会在插件销毁的时候调用栈中的函数,因此将释放资源的函数 push 进去吧。
比如在上面的代码中, vscode.commands.registerCommand 这个函数就会返回释放资源的函数。
-
注册 webview 视图
大致看个眼熟就行,下文“视图”章节会补充细节
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
const provider = new MyWebViewProvider(context.extensionUri);
const dispose = vscode.window.registerWebviewViewProvider('myExtension.webviewView', provider)
context.subscriptions.push(dispose);
}
class MyWebViewProvider implements vscode.WebviewViewProvider {
constructor(private readonly _extensionUri: vscode.Uri) {}
// vscode 会调用这个方法,传入 webviewView 实例,详情下面将
resolveWebviewView(webviewView: vscode.WebviewView) {
webviewView.webview.options = {
enableScripts: true,
localResourceRoots: [this._extensionUri]
};
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
}
private _getHtmlForWebview(webview: vscode.Webview) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My WebView</title>
</head>
<body>
<h1>Hello from My Extension!</h1>
<p>This is a simple WebView in a custom ViewContainer.</p>
</body>
</html>`;
}
}
export function deactivate() {}
-
操作 workspace 配置
案例:注册两个 command 来读取和更新 vscode 配置
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
// 注册一个命令来读取配置
let readConfigCmd = vscode.commands.registerCommand('myExtension.readConfig', () => {
const config = vscode.workspace.getConfiguration('myExtension');
const sampleSetting = config.get('sampleSetting');
vscode.window.showInformationMessage(`Current value: ${sampleSetting}`);
});
// 注册一个命令来更新配置
let updateConfigCmd = vscode.commands.registerCommand('myExtension.updateConfig', async () => {
const config = vscode.workspace.getConfiguration('myExtension');
const newValue = await vscode.window.showInputBox({
prompt: "Enter new value for sampleSetting",
value: config.get('sampleSetting')
});
if (newValue !== undefined) {
await config.update('sampleSetting', newValue, vscode.ConfigurationTarget.Workspace);
vscode.window.showInformationMessage(`Updated sampleSetting to: ${newValue}`);
}
});
context.subscriptions.push(readConfigCmd, updateConfigCmd);
}
export function deactivate() {}
关于 VS Code API 详情请看 VCR:
code.visualstudio.com/api/referen…
一些个人感想
无论是学习框架,语言,一套解决方案...
本质上,我们都是他们的用户,作为用户,可以先想想自己有哪些需求要满足,带着这些需求再审视学习的内容,一些内容就会 “对号入座”,减少学习难度。
3.3 一个值得思考的问题(可跳过)
为什么在package.json 中需要 contributes 这个字段?
再描述清楚一点问题,为什么需要先在 contributes 注册拓展功能,再在口入文件中实现?为什么不直接使用入口文件,省去注册。
下面是我的一些想法:
-
关注点分离
在 package.json 中声明我要干啥。
在 入口文件 中说明怎么干。
用户也可以通过阅读 package.json 大致了解这个插件拓展了哪些内容。
从这个角度来看,方便思考,方便概览。
-
自动集成
插件有一些视图是在插件激活前就需要有的,比如侧边的按钮,或者命令菜单中的选项。因此,他们一定不可能写在入口文件中(因为这个脚本只有插件激活时才初次运行)。
因此,有一些功能是 vscode 根据配置文件自动集成的,而不是通过我们的入口文件手动去写。
比如下文会提到的 viewContainer , vscode 会自动在目标位置渲染在 package.json 中注册的按钮。再比如命令的视图,当你按下 cmd+shift+p ,会在命令菜单中看到我们在 package.json 中注册的命令 title, icon.
从这个角度来看,这是必要的,也降低了开发人员的心智负担。
-
安全性
配置文件是静态的,vscode 分析这个静态能容来了解开发者申请了哪些功能,然后给到不同权限的沙箱。
而且,用户可以通过禁用 contributes 中的某些项,来禁用某部分拓展功能。