1. 背景
想踏入 VS Code 插件领域已经很久了,毕竟 VS Code 是天天都在使用的工具,如果能把一些常用的功能变成一个个插件,比如导入并批量处理 Excel 电子表格、把各类数据转为 Excel 文件等等,那对我们这样的开发者而言,既方便了自己、而能帮助到他人,何乐不为?
只是最近很长一段时间以来,我不是在开发Cordova框架的手机app,就是在开发Electron框架的桌面应用,连后端node.js服务都写得少了不少。而 VS Code 插件开发,对我来说是一个全新的领域,需要有比较长时间、持续不断地培养熟练度,才能快速入门,故一再推迟。不过也得益于长时间的手机app开发,我已经把自己设计的前端框架打磨得比较容易移植,比如正在进行的Cordova、Electron和web页面,比如即将要踏入的 VS Code 插件。
2. 一切从Hello World开始
对于程序员而言,Hello World 是一个信仰。一切都要以它开端,它是我们了解一门新语言、了解一个新框架的不二之选。因此,我们就从官方的Hello World案例开始,了解 VS Code 插件开发的全流程。
2.1 安装脚手架
要开发 VS Code 插件,可以直接使用官方提供的脚手架,可以免去我们逐个配置的麻烦。脚手架需要全局安装:
css
npm install -g yo generator-code
等待安装成功即可。记得给管理员权限。
2.2 新建一个插件项目
在安装好脚手架后,就可以使用 yo
命令来创建新的 VS Code 插件项目了。从命令行进入你的工作区根目录后,执行下面命令,脚手架会协助你录入必要的项目信息。
css
yo code
根据提示填写相关信息:
之后,脚手架会自动安装全部依赖包,并提示是否要为你打开 VS Code;你也可以自行打开项目。
打开项目:
2.3 调试插件
2.3.1 调试
进入 VS Code 后,按 F5
可进行调试(可能需要等待一段时间,取决于你的电脑性能)。运行调试后,会打开一个新的 VS Code 窗口,注意看新窗口顶部有 [Extension Development Host]
的提示。
在新窗口中按下 Shift+Cmd+P
(macOS) 或 Shift+Ctrl+P
(Windows / Linux),输入并选择 Hello world
:
如果你可以在右下角看到这样的消息框,那么恭喜你,你的第一个 VS Code 插件已经可以运行了:
如果你修改了插件的源码,可以在调试工具栏里点击重启按钮,调试窗口会自动刷新,不必先关闭调试窗口:
2.3.2 异常处理
-
如果按下
Shift+Cmd+P
(macOS) 或Shift+Ctrl+P
(Windows / Linux),输入并选择Hello world
找不到符合命令确保 VS Code 版本高于package.json里声明的版本,你可以选择升级 VS Code,也可以选择降低package.json里要求的版本号
-
可以找到命令,但点击运行后报错
这个问题是
out
目录缺失,因为 package.json 文件里声明的入口文件是./out/extension.js
,如果没有编译就直接运行,是无法运行的
这个错误一般不会是使用
F5
进行调试的时候出现的,因为调试命令会自动编译并创建out
目录
2.4 发布插件到插件市场
(详见下文)
3. 基础知识
3.1 VS Code的UX
很多开发者在使用 VS Code 的时候,都是靠以前的习惯或直觉去尝试,实在不知道怎么操作的时候才会去找资料(从侧面说明了 VS Code 的用户动线设计得很合理,学习成本很低)。但对里面到底有哪些组件、可以用来做什么,其实并没有系统地去了解。
但作为 VS Code 插件的开发者(未来甚至可能成为vscode本身代码的贡献者),我们怎么能在完全不懂的情况下去开发呢?因此,在真正上手开始写代码之前,有必要对 VS Code 的UX做一次系统的学习。
相关的官方资料可点击下方链接:
code.visualstudio.com/api/ux-guid...
3.1.1 UI容器Containers和元素Items

主要模块可划分为:
-
Activity Bar
-
Primary Sidebar
-
Secondary Sidebar
-
Editor
-
Panel
-
Status Bar

各模块里可能包含一个或多个下面的元素:
-
View
-
View Toolbar
-
Sidebar Toolbar
-
Editor Toolbar
-
Panel Toolbar
-
Status Bar Item
3.1.2 通用元素
命令面板 Command Palette
这是插件的命令入口,可通过快捷键 Shift+Cmd+P
(macOS) or Shift+Ctrl+P
(Windows / Linux) 调出
Quick Pick
消息通知 Notifications
提供了三类消息通知样式,支持多个选项:
Webviews
给开发者提供了自定义程度很高的途径,如果你有自定义交互内容的UI需求,可以利用好这个功能。请注意,只有在确有必要时才使用。
菜单 Context Menus
为某些特定类型的文件(夹)提供一个上下文菜单,是对命令面板入口的一种补充。请注意,只对必要类型的文件(夹)提供此入口,不要往所有类型的文件都添加上下文菜单(除非确有必要)。
Walkthroughs
插件首次打开时,可以为用户提供一个连贯的设置项向导
Settings
为用户提供配置插件参数的功能
3.2 插件的能力范围
-
通用能力(Common Capabilities)
这是插件的核心功能,包括注册命令、绑定快捷键、管理上下文目录,存储工作台或全局数据,显示提示消息,管理设备文件系统,在耗时较长操作时调用进度条等
-
外观主题管理(Theming)
插件可以控制 VS Code 的外观,包括编辑器里代码的颜色和 VS Code 的 UI 颜色,也可以自定义文件的图标。
-
Declarative Language Features
插件可以声明一种新的编程语言的语法
-
Programmatic Language Features
-
Workbench Extensions
-
Debugging
-
UX Guidelines
-
Restrictions
详情可点击下方链接:
code.visualstudio.com/api/extensi...
3.3 三大概念
3.3.1 激活事件(Activation Events)
即定义激活插件的事件是什么。在第二章Hello World插件案例中,我们定义了一个事件 onCommand:helloworld.helloWorld
,即当用户运行指定命令时、插件将被激活,这个命令的id是 helloworld.helloWorld
。
3.3.2 插件配置清单(Contribution Points)
VS Code 通过在 package.json
的 contributes
字段 中声明插件的配置清单。比如在上面案例里,有一个配置是注册了一个名为 Hello World
的命令,用户可以在命令面板(Command Palette)中找到并执行这个命令;同时,绑定了一个命令Id helloworld.helloWorld
。
json
// package.json
{
...
"contributes": {
"commands": [
{
"command": "helloworld.helloWorld",
"title": "Hello World"
}
]
},
...
}
3.3.3 VS Code API
VS Code 提供了一组JavaScript的api,你可以在自己编写的插件里调用。
在上面的案例里,我们调用了其中一个api commands.registerCommand
,它把一个命令id(helloworld.helloWorld
)与一个 function 绑定起来。这相当于绑定了一个事件监听器:一旦有事件被触发(helloworld.helloWorld
命令),立即执行这个function。
javascript
import * as vscode from "vscode";
export function activate(context: vscode.ExtensionContext) {
const disposable = vscode.commands.registerCommand(
"helloworld.helloWorld", // 命令id
() => {
// 绑定的function
// 定义要执行的内容
}
);
context.subscriptions.push(disposable);
}
export function deactivate() {}
3.4 代码结构
下面是一个插件的默认代码结构:
go
.
├── .vscode
│ ├── launch.json // Config for launching and debugging the extension
│ └── tasks.json // Config for build task that compiles TypeScript
├── .gitignore // Ignore build output and node_modules
├── README.md // Readable description of your extension's functionality
├── src
│ └── extension.ts // Extension source code
├── package.json // Extension manifest
├── tsconfig.json // TypeScript configuration
你需要关注的是两个关键的文件:
-
package.json
:插件配置清单 -
extension.ts
:插件的入口文件
3.5 插件配置文件package.json
下面给出了一个简单的示例(省略了无关的部分):
css
{
"name": "helloworld-sample",
"publisher": "vscode-samples",
"version": "0.0.1",
"displayName": "Helloworld Sample",
"description": "HelloWorld example for VS Code",
"categories": ["Other"],
"engines": {
"vscode": "^1.51.0"
},
"main": "./out/extension.js",
"activationEvents": [],
"contributes": {
"commands": [
{
"command": "helloworld.helloWorld",
"title": "Hello World"
}
]
},
...
}
其中:
-
name
和publisher
分别声明了 VS Code 的插件名称和发布者,在插件市场里,
<publisher>.<name>
是插件的唯一识别码;如果你要发布一个插件,务必准确填写这两个字段。 -
version
插件版本号。每次打包发布插件之前,要更新该字段。
-
displayName
、description
和categories
分别描述了插件的名称、描述信息和分类。其中,
displayName
仅作为显示使用,不会用于识别插件;categories
描述的是该插件所属的分类,内容必须按规则填写。 -
activationEvents
和contributes
分别描述了 Activation Events 和 Contribution Points,是插件的激活方式。上述案例里声明了一个命令,名为
Hello World
,它将在命令面板(Command Palette)出现,其命令id为helloworld.helloWorld
-
main
描述了插件的入口文件
-
engines.vscode
声明了 VS Code API 基于的 VS Code 最低版本。如果当前的 VS Code 版本低于它,将无法调试或使用该插件
3.6 插件入口文件
需要特别注意的是,入口代码文件默认是 ./src/extension.ts
;但在 package.json
中,通过 main
字段声明的则是 ./out/extension.js
,其实它就是代码文件 ./src/extension.ts
编译之后的的结果。
入口文件暴露了两个方法:
-
activate()
当插件注册的激活事件(Activation Event)发生时,该方法将被执行,是插件的入口方法
-
deactivate()
当插件失效时,会调用此方法;你可以在这里做一些数据清理的操作(一般不需要)
下面给出了一个案例,在插件激活并运行后,会弹出一个消息框,内容是"Hello World!"
javascript
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
console.log('Congratulations, your extension "helloworld-sample" is now active!');
let disposable = vscode.commands.registerCommand('helloworld.helloWorld', () => {
vscode.window.showInformationMessage('Hello World!');
});
context.subscriptions.push(disposable);
}
export function deactivate() {}
4. 定制开发一个webview插件
从这一章开始,我将以创建一个Webview插件"贷款计算器"为例,介绍如何进行同类插件的开发。
4.1 开发目标
在命令面板(Command Palette)中输入关键词找到插件后,打开一个新的标签页,在新的页面里可以输入和监听按键,并使用echarts来渲染图表。
4.2 初始化项目
4.2.1 创建项目
按上文所述创建一个新的项目,名为 loan-calculator
。具体步骤略。
4.2.2 添加 contributes.commands
在 package.json
文件里添加 contributes.commands
,这样在按下 Shift+Cmd+P
(macOS) 或 Shift+Ctrl+P
(Windows / Linux)后,就可以选择你的插件了。
json
// package.json
{
"name": "loan-calculator",
"displayName": "Loan Calculator 贷款计算器",
"description": "help to calculate loan cashflow",
"version": "0.0.1",
"publisher": "linxiaozhou",
"engines": {
"vscode": "^1.83.0"
},
"categories": [
"Other"
],
"main": "./out/extension.js",
"activationEvents": [],
"contributes": {
"commands": [
{
"command": "loan-calculator.calc",
"title": "Calculate Loans 计算贷款"
}
]
},
...
}
我们设定了插件的命令id为 loan-calculator.calc
,它的名称为 Calculate Loans 计算贷款
:
4.2.3 注册命令
在入口文件 ./src/extension.ts
中,在 activate()
里调用 VS Code API的 vscode.commands.registerCommand
方法来注册命令 loan-calculator.calc
:
javascript
import * as vscode from "vscode";
// TODO: 等待实现
const openHomePage = (context: vscode.ExtensionContext) => {};
export function activate(context: vscode.ExtensionContext) {
const disposable = vscode.commands.registerCommand(
"loan-calculator.calc", // 命令id
() => {
openHomePage(context);
}
);
context.subscriptions.push(disposable);
}
export function deactivate() {}
4.2.4 打开一个webview
改写 openHomePage()
方法。下面代码将打开一个新的标签页,并在里面显示"贷款计算器""":
ini
const openHomePage = (context: vscode.ExtensionContext) => {
const panel = vscode.window.createWebviewPanel("win", "贷款计算器", vscode.ViewColumn.One);
const webview = panel.webview;
webview.html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>贷款计算器</title>
</head>
<body style="overflow-y: hidden;">
贷款计算器
</body>
</html>
`;
}
按F5进行调试,调出命令面板并找到这个插件后,你将看到这样的内容:
到这里,我们初始化项目的动作就完成了,现在我们正式开始进行插件的开发。
4.3 Webview API
在开发之前,我们需要先了解 VS Code 提供的 Webview API 及用法。
4.3.1 什么是Webview API
在前文中,我们使用的 vscode.window.createWebviewPanel
就是其中一个 Webview API。该方法将返回一个 vscode.WebviewPanel
实例,然后通过 panel.webview.html
设定里面的html内容。
4.3.2 webview环境与extension环境
webview环境 可以认为是一个html沙盒环境,你可以使用html的大部分特性。extension环境则是 VS Code 的插件环境,你可以调用 VS Code API。
从express框架的角度来看,webview环境相当于一个前端,extension环境相当于一个后端。如果你做过Electron开发,则更好理解:webview环境相当于一个渲染进程,extension环境相当于一个主进程;渲染进程是一个权限受限制的环境、用于渲染和交互,主进程则有更高权限(比如可以调用VS Code API),用于处理用户的请求,它的环境里还有插件的上下文context。
4.3.3 设置和刷新webview内容
设置和刷新webview内容只能通过下面方式,刷新方式为刷新整个页面。如果你想局部刷新,应该考虑在webview环境写javascript代码来实现。
下面给出了一个设置和更新的示例:
dart
const panel = vscode.window.createWebviewPanel(...);
panel.webview.html("Hello World"); // 设置内容
panel.webview.html("Hello World too"); // 更新内容
4.3.4 webview的生命周期
在创建webview的panel后,应当秉承"谁创建谁管理"的原则,如果你在创建webview后占用了无法自动释放的资源,应当在webview销毁后销毁相关资源。
官网给了一个简单的示例,示例中,webview里使用了 setInterval()
的方法;如果在此webview销毁之后没有主动清空这个定时器,那么这个资源将无法被释放;仍然会定时执行,从而引起异常。因此,需要在 panel.onDidDispose()
中主动清空这个定时器:
javascript
import * as vscode from 'vscode';
const cats = {
'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif'
};
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{}
);
let iteration = 0;
const updateWebview = () => {
const cat = iteration++ % 2 ? 'Compiling Cat' : 'Coding Cat';
panel.title = cat;
panel.webview.html = getWebviewContent(cat);
};
updateWebview();
const interval = setInterval(updateWebview, 1000);
panel.onDidDispose(
() => {
// When the panel is closed, cancel any future updates to the webview content
clearInterval(interval);
},
null,
context.subscriptions
);
})
);
}
4.4 webview的启动参数
在创建一个webview时,除了设置标签页的标题外,你还可以:
-
设置新打开的标签页放在第几列(column)
-
webview的参数
下图设置了新打开的标签页要放在第2列 vscode.ViewColumn.Two
:
下面给出了一些配置项及作用:
dart
const title = "计算贷款";
const panel = vscode.window.createWebviewPanel(
title, // 视图类型
title, // 面板标题
vscode.ViewColumn.One, // 将webview放在第一列视图中
// Webview 选项
{
// 隐藏时保留webview里的内容,重新显示时不会重新加载
retainContextWhenHidden: true,
// 允许在webview中执行js
enableScripts: true,
// 允许访问本地资源的目录列表
localResourceRoots: [
// [默认] 插件根目录
context.extensionUri,
// 指定其他本地路径
// vscode.Uri.file("本地绝对路径"),
],
}
);
4.5 如何在webview中加载本地图片、js文件等资源
4.5.1 加载插件目录里的资源
你不能直接引用本地的路径或相对路径来加载图片等资源,但可以通过一定的转换指定路径,然后再引用这个路径。如下图所示, 我们加载了一张插件安装目录里的图片:
下面给了一个简单的示例:
ini
export const openDemoWebview = (context: vscode.ExtensionContext) => {
const panel = vscode.window.createWebviewPanel(
"示例",
"示例",
vscode.ViewColumn.One,
{}
);
// 加载插件目录里的图片
const onDiskPath = vscode.Uri.joinPath(
context.extensionUri,
"assets",
"logo.png"
);
// 转换路径
const imgSrc = panel.webview.asWebviewUri(onDiskPath);
panel.webview.html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<img src="${imgSrc}" width="200" />
</body>
</html>
`;
};
4.5.2 加载本地文件系统里任意位置资源
默认情况下,webview只允许加载插件安装目录里的任意资源。如果需要调整为不允许加载任何资源或限制加载的路径,则可通过编辑 vscode.window.createWebviewPanel
的options.localResourceRoots
参数来实现:
-
如果为undefined,即为默认情况,只允许加载插件安装目录里的任意资源
-
如果为空数组
[]
,表示任何本地资源都不可加载 -
有一个或多个
vscode.Uri
元素,表示可以加载指定一个或多个路径下的任意资源
下面给出了一个简单的示例:
ini
export const openDemoWebview = (context: vscode.ExtensionContext) => {
const panel = vscode.window.createWebviewPanel(
"示例",
"示例",
vscode.ViewColumn.One,
{
// 允许访问本地资源的目录列表
localResourceRoots: [
// [默认] 插件根目录
context.extensionUri,
// 指定其他本地路径
vscode.Uri.file("C:/Users/linxiaozhou/Documents/workstation/projects/"),
],
}
);
// 加载插件目录里的图片
const onDiskPath = vscode.Uri.joinPath(
context.extensionUri, "assets", "logo.png"
);
// 转换路径
const imgSrc = panel.webview.asWebviewUri(onDiskPath);
const otherImgSrc = panel.webview.asWebviewUri(
vscode.Uri.file(
`C:/Users/linxiaozhou/Documents/workstation/projects/logo.png`
)
);
panel.webview.html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<img src="${imgSrc}" width="200" />
<img src="${otherImgSrc}" width="200" />
</body>
</html>
`;
};
类比
这一点与express.js框架里设定一个静态资源根目录类似,webview可以控制哪些目录可以作为加载本地资源的根目录;但不同之处在于,express设定的静态目录是作为资源访问的根路径的,即从指定的目录开始写路径,而webview则需要写完整的绝对路径。
如:如果要访问
C:/Users/linxiaozhou/Documents/workstation/projects/
路径下的logo.png
文件,webview需要把完整的路径填入,即C:/Users/linxiaozhou/Documents/workstation/projects/logo.png
,而在express中,设定了上述目录后,只需要填/logo.png
即可。
4.6 在webview中动态加载本地资源
现代网页应用一般都需要支持动态加载js文件,而vscode插件的webview由于做了限制,必须采用https协议和进行路径转换,因此我们不能直接引用一个本地路径。下面我提供了一个解决方案,即每次要加载之前,先使用插件端的能力对需要获取的js文件路径进行转换:
typescript
// extension side
export const convertLocalUrl = (
context: vscode.ExtensionContext,
webview: vscode.Webview,
relativePath: string
) => {
const rawPath = vscode.Uri.joinPath(
context.extensionUri,
"assets",
relativePath
);
const uri = webview.asWebviewUri(rawPath);
return encodeURI(`${uri.scheme}://${uri.authority}${uri.path}`);
};
4.7 Extension与Webview的通信
官方文档给出了两个单向通信的方法:
-
Extension向webview发送消息
-
Webview向extension发送消息
4.7.1 Extension向Webview发送消息
Extension端发送
webview.postMessage(...);
Webview端监听
window.addEventListener('message', event => { ... })
4.7.2 Webview向extension发送消息
Webview端发送
acquireVsCodeApi().postMessage(...)
Extension端监听
webview.onDidReceiveMessage(message => {...}, thisArgs, disposables)
特别注意
acquireVsCodeApi()
是访问VS Code API的入口方法,只能被调用一次,即它返回的实例必须在整个webview的生命周期里维护起来,任何需要调用的地方都只能使用这个实例,而不能重新调用这个方法。
4.7.3 构建一个双向通信的模型
构建一个双向通信的模型,其模型相当于搭建一个C/S架构:
-
webview是客户端,用于发送请求到服务端
-
extension是服务端,用于接收和处理来自客户端的请求
我曾经在旧版本的Electron中遇到过类似的问题(新版本的Electron已经通过提供新的原生API完美解决的此问题)。得益于当时的开发经验,我们可以直接在这里应用。
4.7.3.1 在webview端建立消息中心
下面是消息中心的源码:
javascript
// webview side
// 消息中心
class MessageCenterClass {
constructor(vscode) {
this.queue = {};
this.vscode = vscode
// vscode: 监听从extension来的消息
window.addEventListener("message", async (event) => {
await this.comsumeATask(event);
});
}
generateSeed(length = 20) {
const text = [
"abcdefghijklmnopqrstuvwxyz",
"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
"1234567890",
];
const rand = function (min, max) {
return Math.floor(Math.max(min, Math.random() * (max + 1)));
};
let pw = "s";
for (let i = 0; i < length; ++i) {
const strpos = i === 0 ? 0 : rand(0, 2);
pw += text[strpos].charAt(rand(0, text[strpos].length));
}
return pw;
}
addATask(task) {
const taskId = this.generateSeed();
this.queue[taskId] = task;
return taskId;
}
async comsumeATask(event) {
const data = event.data || {};
const { taskId } = data;
if (!taskId) {
return false;
}
const task = this.queue[taskId];
if (!task) {
return false;
}
return this.queue[taskId](data);
}
send(channel, payload) {
return new Promise((resolve) => {
const taskId = this.addATask(resolve);
this.vscode.postMessage({
command: channel,
taskId,
payload,
});
});
}
}
消息中心的基本逻辑是:
-
客户端发送请求到服务器之前,消息中心会给每一个请求分配一个独立的id、连同消息的payload一起发送到服务端(
vscode.postMessage()
),同时还会把这个请求的回调方法储存起来 -
服务端在处理好本次请求后,把请求的id连同处理后的结果一起发送消息到客户端
-
消息中心在实例化时就会监听从服务端来的事件(
window.addEventListener("message", async (event)=>{ ... })
),在收到服务端消息后,将根据请求的id,从储存的回调列表中找出该请求的回调方法并执行。
而在发送消息到extension环境的方法 send()
这里,我们设置的回调就是Promise里的 resolve
,这样就实现了 async-await
的方式向webview发送消息、异步接收从webview返回的响应:
javascript
send(channel, payload) {
return new Promise((resolve) => {
const taskId = this.addATask(resolve); // 把resolve添加到任务列表中
this.vscode.postMessage({
command: channel,
taskId,
payload,
});
});
}
实例化消息中心并注入vscode API:
ini
// 实例化消息中心并注入vscode API
const vscode = acquireVsCodeApi(); // !!!!!!!! 只能执行一次 !!!!!!!!
const msgCenter = new MessageCenterClass(vscode);
请注意,acquireVsCodeApi()
在整个webview环境的生命周期里只能执行一次。在执行过一次之后如果再次尝试执行,将会报错。
发送消息到extension环境,并接收extension环境的消息:
ini
const res = await msgCenter.send("echo", "hello from webview");
console.log({res});
4.7.3.2 在extension端接收并响应webview端的请求
javascript
const panel = vscode.window.createWebviewPanel(...);
...
// 监听从webview来的消息
webview.onDidReceiveMessage(
(message) => {
const { command: channel, taskId, payload } = message;
switch (channel) {
case "echo":
webview.postMessage({
channel,
taskId,
payload: { message: "echo from extension", echo: payload },
});
return;
// 添加其他事件的监听
// ....
}
},
undefined,
context.subscriptions
);
在extension环境(服务端)里,当webview端(客户端)发送了一个 echo
通道的消息后,extension环境将向webview端返回一个消息 payload
,里面包括 message
和来自webview的 payload
,放在 echo
中。
5. 发布到插件市场
在完成插件的开发和测试后,就可以发布自己的插件啦。你既可以选择手动分发来分享插件,也可以发布到插件市场供大家下载。
5.1 注册账号
要把插件发布到插件市场,你首先得注册账号(不需要备案、认证等杂七杂八的东西,只需要注册就可以发布插件了)。
点击下方链接,按提示即可完成注册:
marketplace.visualstudio.com/manage
5.2 创建一个publisher
在完成注册并登录后,需要创建一个publisher,然后你可以通过这个publisher来发布自己的插件到插件市场。你也可以创建多个publisher来发布不同类型的插件(取决于你的管理模式)。
点击左侧的"Create Publisher":
这边只有两个内容是必填的:
-
Name
publisher的名称,它会展示在插件介绍栏,建议用自己的品牌、公司名称
-
ID
publisher的唯一识别id,一旦确定不可更改。它会在插件的url里出现,同时也需要你把自己的publisher id放在package.json文件的相关字段中
下图给出了publisher的两个信息的示例:
5.3 设置插件名称、版本号和publisher
如果你要发布自己的插件,在打包之前必须先申请成为publisher,获得自己的publisher Id。然后在package.json文件里修改下面几个字段:
注意以下字段描述:
-
name
字段不要随意修改,是识别插件的id -
version
字段在每次发布之前应当修改 -
displayName
理论上是可以随意修改的,但考虑到要留住你的用户或做产品推广,尽量不要随便修改(当然你可以加一些后缀,但关键的信息尽量不变)
5.4 打包
在插件根目录执行即可:
go
vsce package
"vsce"的全称是"Visual Studio Code Extensions",它是一个命令行工具(command-line, CLI),用于打包、发布和管理 VS Code 插件。
注意,每次执行 vsce package
之前会先执行 yarn run compile
;如果你有对源码加扰、复制静态资源的需求,可以在这里添加相关命令。
打包完成后,在插件的根目录会创建一个 .vsix
文件,文件名称格式为 <name>-<version>.vsix
,其中 name
和 version
分别是 package.json 里的 name
字段和 version
字段。
5.5 发布插件
本文以在网页操作为例,介绍如何把打包好的插件发布到插件市场。登录管理页面后,选择一个publisher,然后点击"New Extension",选择"Visual Studio Code":
在弹出框里点击或拖拽打包好的 VSIX 文件到里面:
之后按照提示进行验证即可。需要提醒的是,从你上传插件包到正式发布到插件市场,可能需要一点时间(一般几分钟内可以发布成功)。发布成功后,插件列表会出现绿色的√:
5.6 更新发布
如果你已经发布了一个插件,想要对它进行更新,可以把鼠标移动到需要更新的插件列表右侧的图标,选择"Update"选项;后续操作与添加新插件一致:
需要注意的是,你在打包插件前,package.json 文件里的 name
和 publisher
和已发布的完全一致,且版本号比原来的更高。
5.7 下架一个插件
如果你想下架自己的插件,选择"Unpubish"选项,然后按提示操作即可:
(好端端的干嘛想不开要下架😄)
5.8 手动安装一个插件
当你开发并打包好了一个插件,或者你的朋友开发并打包了一个插件,你可以直接通过 .vsix
文件来安装插件。这样,你或你的朋友就不需要发布到插件市场,就可以分享彼此的插件了。
安装方法为:在 VS Code 打开插件市场,点击右上角的"..."按钮,选择"Install from VSIX...",找到你要安装的插件文件即可。
5.9 一些建议
5.9.1 设置产品图标
是的,一款没有图标的插件实在是让人提不起兴趣。所以无论如何你都要给自己的插件画一个图标。你可以考虑在ppt里用简单的线条和颜色徒手画,如果要用到社区里的图标,请务必确认是否可以商用。
设置插件的图标同样是在package.json文件里,理论上你可以在插件目录下的任意路径来存放图标:
5.9.2 设置LICENSE
在package.json文件里,设置LICENCE,如MIT:
json
{
...
"license": "MIT",
...
}
同时,还需要在插件根目录添加一个 LICENSE
文件:
sql
MIT License
Copyright (c) 2023 linxiaozhou.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
5.9.3 收费or免费
在package.json文件里,设置 pricing
为 Free
:
json
{
...
"pricing": "Free",
...
}
5.9.4 加扰源代码
每次发布之前,通过脚本触发obfuscator对 out
目录里的源码进行加扰。
5.9.5 编写README.md
README.md是插件的详情介绍页,如果你希望更多开发者采用你的插件,务必把你的插件的主要功能、使用方法写清楚,最好还能加上一些运行截图或视频。
怎么上传截图?
如果你希望使用免费的图床,可以在GitHub官网上面直接编辑README.md文件,上传图片或视频。
5.9.6 编写CHANGELOG.md
CHANGELOG.md的内容会出现在插件介绍页面的"CHANGELOG"标签中:
你应当保持干净、清晰的更改记录,冗余的内容(比如PR记录、review内容等)不应该写进去;同时,还可以通过 Unreleased
章节来告知用户后续版本正在开发的功能:
5.9.7 认证Publisher
一个通过认证的Publisher,在插件的信息栏展示Publisher的前面,会有一个徽章:
要进行认证,你需要首先有一个自己的域名、一台可以公网访问的服务器(及一个http服务)。然后按下面步骤操作:
-
进入插件市场后台管理页面,选择要进行验证的Publisher:
-
把一个【合法域名】填入"Domain name",然后保存
-
保存后,选择"Verify",然后把弹出框里给的TXT记录添加到你域名的DNS解析里
-
点击"Verify"来验证



详情请打开下方链接:
code.visualstudio.com/api/working...
血泪教训:除了在域名DNS解析中添加TXT记录以外,服务器务必添加一个http请求监听:
HEAD
请求返回一个200
的状态码,否则不能通过验证。以node.js的express框架为例:
javascript
router.head("/", (req, res) => {
res.status(200).json({ data: "ok" });
});
6. Q&A
6.1 调试时,打开开发者工具后看不到webview里的DOM该怎么办?
点击开发者工具左上角的选择图标,在你的webview页面任意位置点击一下即可。
6.2 加载图片失败怎么办?
请阅读本文的"加载本地资源"一节,然后做以下检查:
-
要加载的图片是否位于插件目录下?
-
如果你设置了允许访问的目录,检查是否在你声明的路径中?或是否已禁用了访问本地资源?(设置项为
localResourceRoots
) -
在引用路径前是否已经通过
webview.asWebviewUri()
方法转换路径? -
如果你访问的是网络资源,检查是否为
http
协议?(必须是https
协议) -
如果你访问的是网络资源,检查内容安全策略(Content security policy)是否禁用了所需资源?
6.3 打开一个webview后,切换到其他标签页再返回原webview,页面会重新加载该怎么办?
在创建此webview的 options
中,设置参数 retainContextWhenHidden = true
即可。
6.4 发布或更新发布插件时,点击"Upload"后无反应
暂时没有别的办法,只能多次尝试,或者想办法科学上网(也不一定可以)
6.5 在webview调用acquireVsCodeApi()方法报错
谨记 acquireVsCodeApi()
方法在开启允许javascript的前提下才能调用,且只能在一个webview里调用一次!
你需要做两个检查:
-
在创建webview的方法里,参数
enableScripts
是否为true
? -
在该webview的所有js代码(包括引用的js文件)里,是否已经有调用过
acquireVsCodeApi()
方法?
7. 参考资料
7.1 官方资料
-
Visual Studio Code Extension API
code.visualstudio.com/api
7.2 其他
-
《从小白到大白 --- 如何开发 VSCode 插件》
mp.weixin.qq.com/s/MJ0r0iRmO... -
百度AI对话