初识
vscode webview 是 vscode 的扩展功能,它允许开发者在 vscode 中创建和嵌入定制的网页视图,可以用来展示复杂的用户界面和交互内容,通常用于开发工具、仪表板、文档查看器等。这些 webview 就像是渲染在 vscode 中的 iframe 一样,几乎支持任何 HTML 内容的渲染,也因此在开发 webview 时可以充分利用现代 Web 技术的能力。
分类
vscode webview 按使用场景来分的话,有三种:
- webview view:用于在 Primary Sidebar 或 Panel 区域(见图一)显示自定义视图。适用于提供辅助功能或信息面板的固定视图,比如日志视图、git 提交记录视图等。
- webview panel:用于在 Editor 区域(见图一)显示独立的、可拖动和关闭的面板。适用于需要大量屏幕空间或用户界面比较复杂的场景,比如仪表板、图表视图等。(参考 webview-panel-sample)
- custom editor view:用于替代 vscode 中的文本编辑器视图,可以完全自定义文件的编辑体验。适用于需要对特定文件类型提供丰富编辑功能的情况,比如图形编辑工具等。(参考 custom-editor-sample)
其中,custom editor view 本质上其实和 webview panel 是一样的,都对应的是 vscode 的 WebviewPanel
类型。而 webview view 对应的则是 WebviewView
类型。由于两者在开发流程和细节上基本大同小异,仅在个别 API 处有差别,故之后所有的内容我们都将围绕 webview view 展开。
三思
虽然 webviews 非常强大,但也应谨慎使用,仅在 vscode 的原生 API 不足以实现某功能时再考虑使用。另外 webviews 资源消耗大,并且它运行在与普通插件不同的上下文环境中。设计不佳的 webview 会很容易让人觉得它与 vscode 本身格格不入。
因此在决定使用 webview 之前,请认真考虑以下三个问题:
- 这个功能是否真的需要在 vscode 内实现?作为单独的 app 或网站会不会更好?
- webview 是实现该功能的唯一途径吗?能否使用其它的 vscode API 来实现?
- 使用 webview 能否带来足够的用户价值以弥补其高资源成本?
如果考虑完以上三个问题,你依然决定要使用 webview 来实现功能,那也请开发前做好比较充分的技术调研,排期时留够 buffer,开发时多考虑下性能等问题。
实践
这部分我们将从零到一完成一个简易版的 to-do list demo 的开发,相关功能设计仅为示例说明,无过多合理性等相关考量..
初始化项目
- 全局安装
yo
及generator-code
。(如果你不想全局安装,可参考 your-first-extension)
npm install --global yo generator-code
- 执行以下命令创建项目。
yo code
图二所示,是我创建项目时所填写的配置。
初始化生成的项目非常简单,我们重点看图三所示即可。
这样当我们在图三中的代码窗口唤起命令行面板(⇧⌘P)并执行 Debug: Start Debugging,就会打开一个新的插件调试窗口(见图四)。在插件调试窗口我们唤起命令行面板后,便可执行图三中注册的 Hello World 命令,然后就可以在窗口右下角看到相应的提示了(见图四)。
添加视图容器
在我们的 demo 中,是需要在 Activity Bar(见图一)中显示插件的 icon。点击 icon 后,在 Primary Sidebar 区域显示相应的插件视图。这样,我们的第一步就必须先定义视图容器了。
定义视图容器的话,在 package.json
的 contributes
属性中配置 viewsContainers
即可。如下代码示例,我们在 Activity Bar 中添加了一个视图容器,并定义了容器的 id
(唯一标识,为容器添加视图定义时会需要)、title
(容器名称,会显示在视图区域顶部)、icon
(插件图标,会显示在 Activity Bar 中)。
{
"contributes": {
"viewsContainers": {
"activitybar": [
{
"id": "toDoList",
"title": "To Do List Demo",
"icon": "media/icon/to-do-list.svg"
}
]
}
}
}
添加视图定义
有了视图容器,接着就得为这些视图容器添加具体的视图信息了。
我们继续为 contributes
属性添加 views
属性即可。如下代码示例,在 views
属性中我们首先需要指明是给哪个视图容器(用 viewsContainers
中定义的视图容器 id 指代)添加视图,之后便可以根据需要添加一个或多个视图信息。在代码示例中,我们添加了三个视图定义,并为每个视图指定了 type
、id
和 name
,分别表示视图的类型(webview
/tree
)、视图唯一标识(这个后面注册视图时会需要)、视图名称(会显示在视图上方)。
{
"contributes": {
"viewsContainers": {
"activitybar": [
{
"id": "toDoList",
"title": "To Do List Demo",
"icon": "media/icon/to-do-list.svg"
}
]
},
"views": {
"toDoList": [
{
"type": "webview",
"id": "addTaskView",
"name": "添加任务"
},
{
"type": "webview",
"id": "toDoListView",
"name": "待办项"
},
{
"type": "webview",
"id": "doneView",
"name": "已完成"
}
]
}
}
}
添加视图内容
有了视图定义,我们还需要为每个视图添加相应的内容,而视图内容的实现,就得依赖于 HTML 了。
以“添加任务”的视图面板为例,我们首先需要定义这样一个 WebviewViewProvider
,如下代码示例,在 AddTaskViewProvider
类中,我们定义了 viewType
这样一个公共静态属性,它的值正是“添加视图定义”中的“添加任务”视图的 id。之后,我们在实现的 resolveWebviewView
方法中通过 webviewView.webview.html
指定了该视图的 HTML 内容,这里的 HTML 必须是完整的 HTML 片段。
import * as vscode from "vscode";
class AddTaskViewProvider implements vscode.WebviewViewProvider {
constructor(private readonly _extensionUri: vscode.Uri) {}
public static readonly viewType = "addTaskView";
public resolveWebviewView(
webviewView: vscode.WebviewView,
context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken
) {
webviewView.webview.html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>To Do List Demo</title>
</head>
<body>
<div>添加任务</div>
</body>
</html>
`;
}
}
export default AddTaskViewProvider;
有了这样一个 WebviewViewProvider
之后,我们还需要在插件中注册下相关视图信息。如下代码示例,我们在插件的激活事件中实例化了上面定义的 AddTaskViewProvider
,并通过 registerWebviewViewProvider
方法注册了该视图实例。这里大家可以注意下该方法的第一个入参,它的值就是“添加任务”视图的 id,到此视图定义、视图内容实现、视图注册就都串起来了。
import * as vscode from "vscode";
import AddTaskViewProvider from "./views/addTaskView";
// This method is called when your extension is activated
// Your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {
const addTaskViewProvider = new AddTaskViewProvider(context.extensionUri);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(
AddTaskViewProvider.viewType,
addTaskViewProvider
)
);
}
同样地,当我们按照上述步骤完成“待办项”和“已完成”这两个视图的接入后,便有了本次 demo 的雏形:
本地调试
如图五中所示,当我们完成了部分功能的开发后,如何快速地预览并调试呢?以下是我的常用步骤:
- 在插件代码工程根目录下执行
pnpm watch
- 在插件代码窗口中唤起命令行面板(⇧⌘P),并执行 Debug: Start Debugging
- 第二步执行后,会打开一个新的插件调试窗口,此时在活动栏中就可以找到我们的插件 icon,点击后即可看到插件的视图,如图五所示。
- 如果你要检查元素样式,可以在命令行面板中输入执行 Developer: Toggle Developer Tools,这时就会出现开发者工具窗口,之后便可以像调试浏览器中的网页一样调试这些 webview,如图六所示:
- 如果你要调试插件代码,可使用 vscode 内置 debug 功能,只需在相应的代码行处添加断点,这样当你打开插件调试窗口后,vscode 就会自动命中这些断点,如图七所示:
- 当你修改了部分代码后,在插件调试窗口,你可以在命令行面板中输入 Developer: Reload Webview 以重新加载所有 webview(或通过 ⌘R 刷新也行)。
添加视图逻辑
有了最简单的插件视图后,我们试着给“添加任务”面板添加一个输入框,并能在输入完内容后点击回车键时拿到输入框的内容。太简单了,我们直接看调整后的 AddTaskViewProvider
的代码:
import * as vscode from "vscode";
class AddTaskViewProvider implements vscode.WebviewViewProvider {
constructor(private readonly _extensionUri: vscode.Uri) {}
public static readonly viewType = "addTaskView";
public resolveWebviewView(
webviewView: vscode.WebviewView,
context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken
) {
webviewView.webview.options = {
// Enable javascript in the webview
enableScripts: true,
};
webviewView.webview.html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>To Do List Demo</title>
</head>
<body>
<input placeholder="添加任务" id="addTaskInput" />
<script>
function toAddTask(e) {
// 按回车键后执行添加逻辑
if (e.keyCode === 13) {
console.log(e.target.value);
}
}
document
.getElementById("addTaskInput")
?.addEventListener("keypress", toAddTask);
</script>
</body>
</html>
`;
}
}
export default AddTaskViewProvider;
因为 webview 默认是不支持执行脚本的,所以代码中我们首先通过设置 webviewView.webview.options.enableScripts = true
来支持执行脚本。之后我们通过内联脚本的方式给输入框绑定了事件,并在回车时拿到了输入框内容。
到这里来看好像也没什么大问题,但当我们需要写大段逻辑或样式时,内联的方式就有点痛苦了。这就引入了如何在 HTML 中加载本地资源的问题。
加载本地资源
出于安全原因,webview 运行在独立的上下文环境中,因此是不能直接访问本地资源的。要在 HTML 中加载本地资源首先需要使用 vscode.Uri.joinPath
拼接资源路径,然后通过 Webview.asWebviewUri
将 file://xxx
格式的本地资源路径转换为特殊的 https://file+.vscode-resource.vscode-cdn.net/xxx
路径,这样 vscode 便可以通过这个特殊路径获取到本地资源。
现在我们将上面的内联脚本迁移到 media/js/addTask.js
中,这时代码看起来就像这样:
public resolveWebviewView(
webviewView: vscode.WebviewView,
context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken
) {
webviewView.webview.options = {
// Enable javascript in the webview
enableScripts: true,
};
const scriptUri = webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, "media", "js", "addTask.js")
);
webviewView.webview.html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>To Do List Demo</title>
</head>
<body>
<input placeholder="添加任务" id="addTaskInput" />
<script src="${scriptUri}"></script>
</body>
</html>
`;
}
好了,现在内联脚本不仅迁移出去而且也能正常的在 HTML 中加载了,其它的样式文件、图片等资源也都是以同样的方式来处理加载。
看到这,那可能有同学就有疑问,你这 media
文件夹是位于哪啊?哪些文件都能被访问呀?
嗯,真是好问题,首先 webview 它能默认访问到的文件范围有两类:一是插件安装目录中的文件(除了 .vscodeignore
中声明的文件,项目中的其他文件都将被打包进安装包并最终解压缩在安装目录中);二是插件运行时所在的工作区的所有文件。
那上面代码中的 this._extensionUri
其实指代的就是插件安装目录的路径,media
文件夹是我在项目根目录创建的且未在 .vscodeignore
中声明,所以它是在默认的可访问范围内的,因此也就可以正常加载执行了。
但为了安全,特别是当我们后面可以加载第三方脚本资源时,可访问范围的限制就至关重要了,这就不得不说下安全策略了。
安全策略
在开发 webview 时,如果我们的 webview 不需要执行脚本,那就不要将上面提到的 enableScripts
设置为 true
了。如果我们需要加载本地资源,最好也通过 localResourceRoots
限制下可访问范围,类似这样:
webviewView.webview.options = {
// Enable javascript in the webview
enableScripts: true,
localResourceRoots: [vscode.Uri.joinPath(this._extensionUri, 'media')],
}
这样在 webview 中就只能访问 media
文件夹中的文件了,或者你不希望访问任何本地文件,那直接将 localResourceRoots
设置为 []
即可。
仅限制了文件可访问范围还不够,我们还应该在 HTML 的 head
标签顶部添加 <meta http-equiv="Content-Security-Policy" content="default-src 'none';">
这样一个标签,这样在 webview 中将不能加载任何外联的脚本、样式,图片及音视频等资源,从而将权限收至最小。之后我们如果要限制只能加载本地的脚本、样式及图片等资源,图片可同时限制只支持加载 https
资源, 就可以像这样按需扩展内容安全策略:
<meta
http-equiv="Content-Security-Policy"
content="default-src 'none'; img-src ${webview.cspSource} https:; script-src ${webview.cspSource}; style-src ${webview.cspSource};"
>
需注意的是,这样添加安全策略后,内联的脚本及样式也不再生效,最佳的实践肯定还是推荐将所有脚本及样式都抽离出去然后通过外联的方式加载执行。(注:其它安全策略的扩展可参考 内容安全策略)
另外,对于脚本的加载执行,我们还可以更进一步地限制下,只允许特定的脚本执行,类似这样:
private _getHtmlForWebview(webview: vscode.Webview) {
// Get the local path to the script run in the webview, then convert it to a uri we can use in the webview.
const scriptUri = webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, "media", "js", "addTask.js")
);
// Use a nonce to only allow a specific script to be run.
const nonce = getNonce();
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!--
Use a content security policy to only allow loading styles from our extension directory,
and only allow scripts that have a specific nonce.
-->
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-${nonce}';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>To Do List Demo</title>
</head>
<body>
<input placeholder='添加任务' id='addTaskInput' />
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>
`;
}
其中 nonce
是每次加载 webview 时都会随机生成的一个字符串,这样也就只有带了 nonce
属性,并且 nonce
属性值和内容安全策略中的 nonce
值匹配的脚本可以加载执行了。
最后,如果你有依赖于用户输入的内容来动态构建 HTML,那一定也得考虑并防范下内容注入的风险。
数据通信
有了上面的介绍,在 webview 中加载脚本、样式、图片等本地资源就不成问题了。接下来我们继续完善 demo 的功能。
在“添加视图逻辑”一节中,我们已经可以从输入框中获取用户填写的内容了,这一节我们就来实现以下功能:
- 在“添加任务”面板输入完内容并点击回车键后,往“待办项”面板添加一条待办任务。
- 在“待办项”面板中显示所有待办任务,点击某一条待办任务后,则该任务状态变为已完成并添加进“已完成”面板。
- 在“已完成”面板中显示所有已完成任务,点击某一条已完成任务后,则该任务状态变为待完成并添加进“待办项”面板。
好了,那对于第一点中提到的,我们要如何将用户填写的内容添加至“待办项”面板呢?这就涉及到 webview 与插件的通信问题了。
不同的 webview 就如同相互独立的 iframe,它们之间自然是不能直接通信的。但 webview 本身受插件控制,因此 webview 是可以和插件相互通信的。我们要做就是将“添加任务” webview 中的数据先同步给插件,然后再由插件将数据同步给“待办项” webview。
webview to extension
首先我们来看 webview 如何发送数据,以下是 “添加任务” webview 脚本中的相关实现:
// @ts-check
// This script will be run within the webview itself
// It cannot access the main VS Code APIs directly.
(function () {
// @ts-ignore
const vscode = acquireVsCodeApi();
function toAddTask(e) {
// 按回车键后执行添加逻辑
if (e.keyCode === 13) {
vscode.postMessage({ type: "addTask", content: e.target.value });
}
}
document
.getElementById("addTaskInput")
?.addEventListener("keypress", toAddTask);
})();
可以看到,我们首先调用了默认会被挂载在全局的 acquireVsCodeApi
来获取 vscode 的 api 对象,之后便可以通过这个 api 对象的 postMessage
方法同步数据到插件。那插件中又是如何接收来自 webview 中的数据呢?以下是我在插件中的相关实现:
public resolveWebviewView(
webviewView: vscode.WebviewView,
context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken
) {
setWebview(ViewType.addTaskView, webviewView.webview);
webviewView.webview.options = getWebviewOptions(this._extensionUri);
webviewView.webview.onDidReceiveMessage((data) => {
switch (data.type) {
case "addTask": {
this._toAddTask(data.content);
break;
}
}
});
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
}
可以看出,在插件侧,我们需要调用 webview 实例的 onDidReceiveMessage
方法来添加一个监听器,用以监听来自 webview 内部的消息,监听器函数的入参值即是上面调用 postMessage
方法时传的数据。到这里我们的插件就能正常收到来自 webview 的数据了,下面我们继续看如何将数据从插件侧同步到 webview 内。
extension to webview
继续以我们的 demo 为例,以下是插件侧收到用户输入的内容后,同步数据给“待办项” webview 的相关代码:
public resolveWebviewView(
webviewView: vscode.WebviewView,
context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken
) {
setWebview(ViewType.addTaskView, webviewView.webview);
webviewView.webview.options = getWebviewOptions(this._extensionUri);
webviewView.webview.onDidReceiveMessage((data) => {
switch (data.type) {
case "addTask": {
this._toAddTask(data.content);
break;
}
}
});
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
}
private _toAddTask(data: string) {
const toDoListView = getWebview(ViewType.toDoListView);
if (toDoListView) {
toDoListView.postMessage({
type: "addTask",
data,
});
}
}
在 _toAddTask
方法中,我们首先获取了“待办项” webview 实例(具体实现见 webview-cache),然后也是调用 webview 实例的 postMessage
方法来发送数据到 webview 内部。发送后,webview 内部是这样来接收数据的:
// Handle messages sent from the extension to the webview
window.addEventListener("message", (event) => {
// The json data that the extension sent
const message = event.data;
switch (message.type) {
case "addTask":
return toUpdateToDoList(message.data);
}
});
直接调用 window.addEventListener
添加 message
事件即可,如代码所示,event.data
即就是插件侧发送过来的数据。
好的,到这里我们就解决了将用户填写的内容同步至“待办项”面板的问题了。本节我们要实现的“待办项”面板和“已完成”面板之间的数据同步问题也都是一样的处理方式,另外两个任务列表的实现这里也就不再展开了(具体实现见 toDoList.js)。现在我们可以看看本节功能实现后的 demo 效果了:
数据持久化
在上一节最后的 demo 视频里,观察仔细的同学们应该已经发现当我在收起又展开“待办项”面板后,添加的待办项任务都消失了。这是因为当 webview 内容被隐藏后,之前运行时所产生的一些状态数据也就随之丢失了,所以再次展开面板后,我们看到的又是一个没有任何初始数据的空面板。要解决这个问题也简单,我们只需使用 setState
和 getState
来做数据的持久化缓存和获取即可。
setState
我们以“待办项” webview 的脚本为例,在调用 acquireVsCodeApi
获得 vscode api 对象后,继续调用该 api 对象的 setState
方法即可缓存任何 JSON 序列化对象:
const vscode = acquireVsCodeApi();
function updateToDoList(tasks) {
// 持久化缓存最新的待办项任务
vscode.setState({ toDoList: tasks });
// 动态操作 dom 以同步最新数据到面板中
}
getState
同样地,在 webview 脚本中调用 vscode api 对象的 getState
方法即可获取该 webview 的所有缓存数据:
const vscode = acquireVsCodeApi();
const toDoListState = vscode.getState() || {
toDoList: [],
};
let toDoList = toDoListState.toDoList;
优化后,无论是收起后再展开“待办项”面板,亦或是切换到其它插件面板后再回来,甚至是关闭当前 vscode 窗口后再重新打开并查看“待办项”面板,持久化的数据将一直存在,以下是优化后的 demo 效果:
注意,这里提到的持久化数据的相关表现仅针对 WebviewView
,在 WebviewPanel
中的现象可能稍有不同。另外,对于复杂一点的 WebviewPanel
,还可使用 retainContextWhenHidden
作为持久化数据的另一种方案,但出于性能等原因,setState
和 getState
无疑是更推荐的方案。具体可参考 webview-persistence。
视图操作栏
现在我们的任务数据也能持久化存储了,但总感觉还是少了点什么,想了想还是给“已完成”视图面板添加一个一键清空的操作吧。以下是大概实现:
一,在 package.json
的 contributes
属性中这样配置下 commands
和 menus
:
{
"contributes": {
"commands": [
{
"command": "to-do-list-demo.clearDoneList",
"title": "To Do List Demo: 清空已完成任务",
"icon": "$(notebook-delete-cell)"
}
],
"menus": {
"view/title": [
{
"command": "to-do-list-demo.clearDoneList",
"group": "navigation",
"when": "view === doneView"
}
]
}
}
}
这样,在“已完成”面板的右上角便会显示命令 to-do-list-demo.clearDoneList
对应的 icon,点击 icon 便会执行该命令对应的逻辑。这里我们解释几个关键点:
- 当操作本身比较高频时,可配置
view/title
的group
属性为"navigation"
,这样便可在面板标题的右侧以行内的方式展示出对应命令的 icon,否则都将显示在面板右上角的...
菜单内。 - 如果你只想在某一个面板显示相关的操作项,配置
when
属性即可。上面示例中,我们就限制了只在“已完成”(doneView)面板中显示删除操作。 - 配置命令的 icon 时,推荐优先使用内置 icon(可参考 vscode-icons)。当内置 icon 不能满足需求时,也可以通过这种方式指定下深色和浅色主题下的 icon 文件路径:
{
"contributes": {
"commands": [
{
"command": "to-do-list-demo.clearDoneList",
"title": "To Do List Demo: 清空已完成任务",
"icon": {
"light": "media/icon/clear_light.svg",
"dark": "media/icon/clear_dark.svg"
}
}
]
}
}
二,在插件的激活事件中注册下相关命令:
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("to-do-list-demo.clearDoneList", () => {
doneViewProvider.clearDoneList();
})
);
}
这里需注意,使用 registerCommand
注册的命令必须事先在 package.json
中定义好,并且保证拼写等都正确。clearDoneList
方法的细节这里就不展开了(具体实现见 doneView.ts)。此时的 demo 效果是这样的:
插件配置
好了,接下来我们再来看看如果我们的插件还需要一些配置项,应该怎么来定义。很简单,我们直接上代码:
{
"contributes": {
"configuration": {
"title": "To Do List Demo",
"properties": {
"toDoListDemo.toDoListMaxCount": {
"type": "number",
"description": "待办项最大数量",
"default": 10,
"minimum": 1
}
}
}
}
}
仍然是给 package.json
的 contributes
属性配置 configuration
即可,其中 title
属性指代配置的标题,properties
属性具体描述有哪些配置项。这里我们添加了一个“待办项最大数量”的配置,它的类型是 number
,默认值是 10
,最小值是 1
(其它配置类型可参考 vscode-configuration)。完成后,我们在插件调试窗口中打开 vscode 设置面板,即可搜索到上面添加的插件配置:
之后,在插件代码中,我们便可这样来获取插件相关配置项的值:
import * as vscode from "vscode";
export function getToDoListMaxCount() {
return vscode.workspace.getConfiguration().get<number>('toDoListDemo.toDoListMaxCount');
}
当然,如果你要实时监听某些配置项的值的变化,通过以下方式即可实现:
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from "vscode";
import { getToDoListMaxCount } from "src/utils";
// This method is called when your extension is activated
// Your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration(() => {
const maxCount = getToDoListMaxCount();
console.log("待办项最大数量", maxCount)
})
);
}
WebviewView 事件
onDidDispose
onDidDispose
事件会监听 webview 的销毁,当 webview 销毁的时候,会触发该事件的执行。与 WebviewPanel
触发销毁的方式不同,仅当把一个 WebviewView
视图从一个视图区域拖至另一个视图区域时才会触发销毁事件,比如将 WebviewView
视图从 Primary Sidebar 区域拖拽至 Panel 或 Secondary Sidebar 区域时就会触发被拖拽视图的销毁事件。
你可以在 onDidDispose
回调中做一些取消定时器任务、释放 webview 占用资源的逻辑等,代码示例如下:
public resolveWebviewView(
webviewView: vscode.WebviewView,
context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken
) {
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
webviewView.onDidDispose(() => {
// 可以在这里做一些取消定时器任务、释放 webview 占用资源的逻辑等
})
}
onDidChangeVisibility
onDidChangeVisibility
事件会监听 webview 可见性的变化,你通常需要结合 WebviewView.visible
来做一些业务逻辑的处理,类似这样:
public resolveWebviewView(
webviewView: vscode.WebviewView,
context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken
) {
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
webviewView.onDidChangeVisibility(() => {
// 当 webview 不可见时
if (!webviewView.visible) {
// 你可以在这里做一些取消定时器任务、中断 ws 连接等占用资源的逻辑
}
})
}
生命周期
正如 react、vue 等组件都有自己的生命周期一样,vscode 插件也有自己的生命周期。在通过 yo code
生成的插件工程中,插件入口文件 src/extension.ts
默认暴露了两个方法,一个是我们上面频繁提到的插件激活事件 activate
,另一个则是插件销毁时会被调用的 deactivate
方法,这两个便是 vscode 插件生命周期的两个钩子方法。
activate
以我们的 demo 为例,触发 activate
方法的时机有两个,分别是:
- 唤起命令行面板,执行在插件中注册的命令
to-do-list-demo.clearDoneList
- 点击活动栏中的插件 icon,此时因为要显示相应的插件视图,也会触发
activate
方法
在同一个 session 中,activate
方法仅会执行一次,我们一般会在其中注册视图信息、注册插件命令、监听插件的配置变化等。更多会触发 activate
方法的事件可查阅 vscode-activation-events。
deactivate
deactivate
方法会在关闭插件所在的 vscode 窗口时触发。我们一般会在里面执行一些清理插件所占资源的逻辑,比如取消定时器任务、中断 ws 连接、终止 web workers 等。
这里需注意,如果你的清理逻辑是同步的,那 deactivate
方法返回 undefined
即可,而如果是异步的,则必须返回 Promise
。
小结
okkk,本文篇幅已经够长的了.. 这里我们简单小结下吧:
在“初识”及“分类”中,我们希望大家能对 vscode webview 有一个整体的感知,它是什么?能用来做什么?能满足哪些场景?
之后的“三思”一段也是官方的提示,希望大家能在了解了 webview 的能力圈及弊端后,仔细审视自己的需求,到底适不适合用 webview 来实现?
再之后的实战中,我们以开发者的视角,一步一步从零到一完成了一个简单 demo 的开发,并尽可能地覆盖了大多数情况下都会用到的知识点。大家有兴趣的话,下载 demo 源码后,可以自己改改代码实际调试感受下 webview 的能力范围,比如如何在 Panel 中定义视图容器?定义多个视图容器或者不定义视图容器又会怎样?不定义视图容器时 contributes.views
还能怎么配置?效果是怎样的?相信实际调试后,当大家开发自己的插件时,一定能做出最合理的规划!
最后,本篇完成的 demo 实在太过粗糙了,不仅视觉效果差点,比如原生的 HTML input 在 vscode 中就显得很突兀,而且开发体验也差,写的都是原生的 html、css、js,根本没用到任何开篇中提到的“现代 Web 技术的能力”。那下一篇我们就来说说如何使用 react、vscode-webview-ui-toolkit 等开发 webview 插件以及主题色等相关内容。