webview API
允许插件在 Visual Studio Code
中创建高度定制化的视图,例如内置的Markdown
插件就是利用webview
来渲染Markdown
预览页的。webview
还可以创建出比VS Code
原生API
所直接支持的更加复杂的用户界面。
可以将webview
视为VS Code
下插件所控制的iframe
,webview
可以渲染出任意的HTML
内容并通过信息(message
)机制和插件通信,这种高度自由使得webview
的能力十分强大,令其开启了插件可能性的新领域
webview
在以下一些场景中被使用:
- 用
createWebviewPanel
创建一个webview
面板,在VS Code
中地位如同一个单独的编辑界面,这在展示自定义UI和自定义的可视化方面非常有用 - 作为 custom editor 的一个视图存在, custom editor 允许插件提供一个自定义UI去编辑工作区中的任何的文件,
custom editor API
提供了一系列编辑事件,如撤销、保存等 - 在
sidebar
或panel
中渲染,查看 webview view sample 获取更多信息
API
使用场景
webview
功能十分强大,不过我们不应滥用,只有当VS Code
原生API
无法支持时才建议使用webview
。webview
涉及到的资源较多,和普通插件相比其运行在一个单独的环境里,这就使得一个设计不好的webview
会感到和VS Code
有些不相称。在使用webview
前,请仔细考虑下面这些问题:
- 该功能真的适合放在
VS Code
中而不是一个单独的app或网站么? webview
是唯一实现的途径么?VS Code API
能否直接支持?- 是否评估过
webview
中的资源成本?
使用方法
基础示例
为了说明Webviews API
的用法,我们会创建一个简单的插件,这个插件将会利用一个webview
来展示一幅图片。
下面是这个插件第一个版本的package.json
,可以看到其定义了一个命令catCoding.start
,当用户触发了这个命令,我们将会展示有猫咪图片的webview
{
"name": "webview-test",
"displayName": "webviewTest",
"description": "",
"version": "0.0.1",
"engines": {
"vscode": "^1.56.0"
},
"categories": [
"Other"
],
"activationEvents": ["onCommand:catCoding.start"],
"main": "./dist/extension.js",
"contributes": {
"commands": [{
"command": "catCoding.start",
"title": "Start new cat coding session",
"category": "Cat Coding"
}]
},
"scripts": {
"vscode:prepublish": "yarn run package",
"compile": "webpack",
"watch": "webpack --watch",
"package": "webpack --mode production --devtool hidden-source-map",
"test-compile": "tsc -p ./",
"test-watch": "tsc -watch -p ./",
"pretest": "yarn run test-compile && yarn run lint",
"lint": "eslint src --ext ts",
"test": "node ./out/test/runTest.js"
},
"devDependencies": {
"@types/vscode": "^1.56.0",
"@types/glob": "^7.1.3",
"@types/mocha": "^8.0.4",
"@types/node": "14.x",
"eslint": "^7.19.0",
"@typescript-eslint/eslint-plugin": "^4.14.1",
"@typescript-eslint/parser": "^4.14.1",
"glob": "^7.1.6",
"mocha": "^8.2.1",
"typescript": "^4.1.3",
"vscode-test": "^1.5.0",
"ts-loader": "^8.0.14",
"webpack": "^5.19.0",
"webpack-cli": "^4.4.0"
}
}
复制代码
接下来让我们注册catCoding.start
命令去打开webview
:
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
// Create and show a new webview
const panel = vscode.window.createWebviewPanel(
'catCoding', // Identifies the type of the webview. Used internally
'Cat Coding', // Title of the panel displayed to the user
vscode.ViewColumn.One, // Editor column to show the new webview panel in.
{} // Webview options. More on these later.
);
})
);
}
复制代码
vscode.window.createWebviewPanel
函数打开了一个webview
,当我们执行完catCoding.start
命令后,将会打开一个空白的webview
:
我们可以看到这个空白的webview
已经有了正确的标题,为了渲染出想要的内容,我们还需要利用webview.html
来指定内容的html
:
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
// Create and show a new webview
const panel = vscode.window.createWebviewPanel(
'catCoding', // Identifies the type of the webview. Used internally
'Cat Coding', // Title of the panel displayed to the user
vscode.ViewColumn.One, // Editor column to show the new webview panel in.
{} // Webview options. More on these later.
);
// And set its HTML content
+ panel.webview.html = getWebviewContent();
})
);
}
+ function getWebviewContent() {
+ return `<!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="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
+ </body>
+ </html>`;
+ }
复制代码
webview.html
必须是完整的html
文档,如果是html
片段可能会引发意料之外的问题
刷新内容
webview.html
可以在创建完毕后进行更新,接下来让我们改进一下代码,增加动态切换图片的能力:
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);
+ };
// Set initial content
updateWebview();
+ // And schedule updates to the content every second
+ setInterval(updateWebview, 1000);
})
);
}
function getWebviewContent(cat: keyof typeof cats) {
return `<!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="https://juejin.cn/post/${cats[cat]}" width="300" />
</body>
</html>`;
}
复制代码
设置webview.html
会替换掉整个webview
的内容,类似重新加载iframe
,如果你在webview
中使用了js
脚本这点就很重要,需要注意设置了webview.html
会导致脚本的状态被重置。需要留意上文的示例还使用了webview.title
来改变标题,重置标题不会导致webview
的重新加载
生命周期
webview
面板属于创建它的插件,通过获取createWebviewPanel
的返回对象来操控webview
。对于用户来说,可以随时关闭webview
面板,一旦被关闭,则webview
将会被销毁,尝试去引用一个被销毁的webview
会报错,所以对于上述代码来说,setInterval
的使用存在bug。当用户关闭了webview
面板后setInterval
依然会被不断地触发,下面让我们来看下如何处理这个问题。
当webview
面板被销毁时会触发onDidDispose
事件,我们可以在此来取消setInterval
:
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', () => {
// Create and show a new webview
const panel = vscode.window.createWebviewPanel(
'catCoding', // Identifies the type of the webview. Used internally
'Cat Coding', // Title of the panel displayed to the user
vscode.ViewColumn.One, // Editor column to show the new webview panel in.
{} // Webview options. More on these later.
);
let iteration = 0;
const updateWebview = () => {
const cat = iteration++ % 2 ? 'Compiling Cat' : 'Coding Cat';
panel.title = cat;
panel.webview.html = getWebviewContent(cat);
};
// Set initial content
updateWebview();
// And schedule updates to the content every second
+ 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
+ );
})
);
}
function getWebviewContent(cat: keyof typeof cats) {
return `<!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="https://juejin.cn/post/${cats[cat]}" width="300" />
</body>
</html>`;
}
复制代码
对于插件来说,也可以通过调用dispose
关闭webview
面板,例如我们可以控制在五秒之后关闭webview
面板:
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
// Create and show a new webview
const panel = vscode.window.createWebviewPanel(
'catCoding', // Identifies the type of the webview. Used internally
'Cat Coding', // Title of the panel displayed to the user
vscode.ViewColumn.One, // Editor column to show the new webview panel in.
{} // Webview options. More on these later.
);
let iteration = 0;
const updateWebview = () => {
const cat = iteration++ % 2 ? 'Compiling Cat' : 'Coding Cat';
panel.title = cat;
panel.webview.html = getWebviewContent(cat);
};
// Set initial content
updateWebview();
// And schedule updates to the content every second
const interval = setInterval(updateWebview, 1000);
// After 5sec, programmatically close the webview panel
+ const timeout = setTimeout(() => {
+ clearInterval(interval);
+ panel.dispose();
+ }, 5000);
panel.onDidDispose(
() => {
// When the panel is closed, cancel any future updates to the webview content
clearInterval(interval);
+ clearTimeout(timeout);
},
null,
context.subscriptions
);
})
);
}
复制代码
可以看到依次发生了:执行panel.dispose
->触发panel.onDidDispose
->webview
面板关闭
可见性与移动
当我们在编辑区切换tab
,将webview
面板置于不可见的状态时,webview
面板实际上没有被销毁VS Code
会将webview
内容存储起来,当webview
面板切换回来时继续使用。可以通过visible
属性获取到当前webview
面板可见性的状态。插件可以通过reveal
方法将被切换到隐藏的webview
重新切到可见状态,方法接受一个表明视图列的参数。
下面来优化下代码,如果webview
已经被创建,则将其切换到可见状态(在前面的版本里,执行命令会不断生成新的webview
):
export function activate(context: vscode.ExtensionContext) {
// Track currently webview panel
let currentPanel: vscode.WebviewPanel | undefined = undefined;
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const columnToShowIn = vscode.window.activeTextEditor
? vscode.window.activeTextEditor.viewColumn
: undefined;
if (currentPanel) {
// If we already have a panel, show it in the target column
currentPanel.reveal(columnToShowIn);
} else {
// Otherwise, create a new panel
currentPanel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
columnToShowIn,
{}
);
currentPanel.webview.html = getWebviewContent('Coding Cat');
// Reset when the current panel is closed
currentPanel.onDidDispose(
() => {
currentPanel = undefined;
console.log('onDidDispose');
},
null,
context.subscriptions
);
}
})
);
}
复制代码
当webview
的可见性或所处的列发生变动时,会触发onDidChangeViewState
事件,我们可以改造下插件,让它在所处不同视图列时展示不同的内容:
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',
'Testing Cat': 'https://media.giphy.com/media/3oriO0OEd9QIDdllqo/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,
{}
);
panel.webview.html = getWebviewContent('Coding Cat');
// Update contents based on view state changes
panel.onDidChangeViewState(e => {
const curPanel = e.webviewPanel;
console.log(panel.viewColumn, curPanel.viewColumn);
switch (curPanel.viewColumn) {
case vscode.ViewColumn.One:
updateWebviewForCat(curPanel, 'Coding Cat');
return;
case vscode.ViewColumn.Two:
updateWebviewForCat(curPanel, 'Compiling Cat');
return;
case vscode.ViewColumn.Three:
updateWebviewForCat(curPanel, 'Testing Cat');
return;
}
}, null, context.subscriptions);
})
);
}
function updateWebviewForCat(panel: vscode.WebviewPanel, catName: keyof typeof cats) {
panel.title = catName;
panel.webview.html = getWebviewContent(catName);
}
function getWebviewContent(cat: keyof typeof cats) {
return `<!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="https://juejin.cn/post/${cats[cat]}" width="300" />
</body>
</html>`;
}
复制代码
检查和调试
运行Developer: Toggle Developer Tools
命令可以调试webview
如果你的VS Code
版本低于1.56
或设置了enableFindWidget
,则需要执行Developer: Open Webview Developer Tools
命令,这个命令给每个webview
开启了一个单独的Developer Tools
而不是所有的webview
共用同一个。
命令Developer: Reload Webview
会重载所有已经开启的webview
,当希望重置webview
的内部状态或重新加载资源时可以用到。
加载本地资源
webview
通过localResourceRoots
选项来控制哪些资源可以从用户本地加载,localResourceRoots
定义了一系列允许本地加载的根URI
。例如我们可以通过localResourceRoots
来限制插件只能从本地的media
文件夹加载资源:
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,
{
// Only allow the webview to access resources in our extension's media directory
localResourceRoots: [vscode.Uri.file(path.join(context.extensionPath, 'media'))]
}
);
const onDiskPath = vscode.Uri.file(
path.join(context.extensionPath, 'media', 'cat.gif')
);
const catGifSrc = panel.webview.asWebviewUri(onDiskPath);
panel.webview.html = getWebviewContent(catGifSrc);
})
);
}
复制代码
如果希望不允许加载本地任何资源,将localResourceRoots
设置成空数组即可。总的来说在webview
中我们还是应该尽可能的限制对本地资源的加载,localResourceRoots
并不提供全面的安全保护,有关安全性方面我们后面会有所介绍。
适配主题
我们可以用css
来给webview
附加匹配当前主题对的样式,VS Code
的主题有三大类:vscode-light
、vscode-dark
、vscode-high-contrast
。VS Code
会将当前主题的类别附加在webview
的body
元素的class
上
然后我们可以编写类似如下的代码适配不同的主题:
body.vscode-light {
color: black;
}
body.vscode-dark {
color: white;
}
body.vscode-high-contrast {
color: red;
}
复制代码
VS Code
官方要求当开发一款webview
应用时,需要确保其支持三种主题,并且一定要在high-contrast
做好测试以便对于存在视力缺陷的用户也是可以使用的。
在编写webview
样式文件时,我们可以使用 css 变量 来访问VS Code
内置的主题色,这些变量是主题色变量附加--vscode-
前缀并将.
替换成-
,例如editor.foreground
的css
变量是var(--vscode-editor-foreground)
:
code {
color: var(--vscode-editor-foreground);
}
复制代码
你可以在 theme-color 找到VS Code
所支持的主题色,你可以通过安装 VS Code CSS Theme Completions 来得到变量名称的自动补全。
另外你要还需要知道一些和字体有关的变量:
--vscode-editor-font-family
:数值同editor.fontFamily
--vscode-editor-font-weight
:数值同editor.fontWeight
--vscode-editor-font-size
:数值同editor.fontSize
如果你希望给某个具体的主题适配css
样式,你可以利用body
上的vscode-theme-name
属性,该属性值是当前使用的主题名称
body[data-vscode-theme-name="Darktooth"] {
background: hotpink;
}
复制代码
脚本和消息传递
脚本
webview
和iframe
很相似,你可以运行自己的脚本,默认情况下脚本是不可用状态,需要通过设置enableScripts: true
来开启该功能,让我们编写一段简单的计数脚本,不过这仅是个示例,实际编写的时候不要使用内联的脚本(内联代码被视为是有害的)
import * as vscode from 'vscode';
import * as path from 'path';
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,
{
// Enable scripts in the webview
enableScripts: true
}
);
panel.webview.html = getWebviewContent();
})
);
}
function getWebviewContent() {
return `<!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="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
<h1 id="lines-of-code-counter">0</h1>
<script>
const counter = document.getElementById('lines-of-code-counter');
let count = 0;
setInterval(() => {
counter.textContent = count++;
}, 100);
</script>
</body>
</html>`;
}
复制代码
webview
中的脚本使用起来和普通网页里的脚本一样,能干的事情也一样,没有区别。需要注意的是,webview
中的脚本不能直接调用VS Code API
,如有此类需要,需借助消息机制来实现
消息传递
从插件向webview传递消息
对于插件来说,可以通过webview.postMessage()
函数向webview
传递JSON
数据,在webview
中通过监听message
事件(window.addEventListener('message', event => { ... })
)来接收数据。为了演示数据传递的功能,我们增加一个新的命令catCoding.doRefactor
,当执行这个新命令时传递给webview
一条信息,让计数器计数减半:
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
// Only allow a single Cat Coder
let currentPanel: vscode.WebviewPanel | undefined = undefined;
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
if (currentPanel) {
currentPanel.reveal(vscode.ViewColumn.One);
} else {
currentPanel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{
enableScripts: true
}
);
currentPanel.webview.html = getWebviewContent();
currentPanel.onDidDispose(() => {
currentPanel = undefined;
}, undefined, context.subscriptions);
}
})
);
// Our new command
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.doRefactor', () => {
if (!currentPanel) {
return;
}
// Send a message to our webview.
// You can send any JSON serializable data.
currentPanel.webview.postMessage({ command: 'refactor' });
})
);
}
function getWebviewContent() {
return `
<!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="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
<h1 id="lines-of-code-counter">0</h1>
<script>
const counter = document.getElementById('lines-of-code-counter');
let count = 0;
setInterval(() => {
counter.textContent = count++;
}, 100);
// Handle the message inside the webview
window.addEventListener('message', event => {
const message = event.data; // The JSON data our extension sent
switch (message.command) {
case 'refactor':
count = Math.ceil(count * 0.5);
counter.textContent = count;
break;
}
});
</script>
</body>
</html>`;
}
复制代码
从webview向插件传递消息
webview
可以给插件传递信息,利用webview
内嵌的VS Code API
对象来调用postMessage
函数,为了获取到VS Code API
对象需要调用acquireVsCodeApi
函数。acquireVsCodeApi
函数只能调用一次,所以你需要将其返回的内容保存好,以便其它脚本可以调用到:
import * as vscode from 'vscode';
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,
{
enableScripts: true
}
);
panel.webview.html = getWebviewContent();
// Handle messages from the webview
panel.webview.onDidReceiveMessage(message => {
switch (message.command) {
case 'alert':
vscode.window.showErrorMessage(message.text);
return;
}
}, undefined, context.subscriptions);
})
);
}
function getWebviewContent() {
return `
<!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="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
<h1 id="lines-of-code-counter">0</h1>
<script>
(function() {
// 获取VS Code API 对象
const vscode = acquireVsCodeApi();
const counter = document.getElementById('lines-of-code-counter');
let count = 0;
setInterval(() => {
counter.textContent = count++;
// Alert the extension when our cat introduces a bug
if (count % 100 === 0) {
vscode.postMessage({
command: 'alert',
text: '? on line ' + count
})
}
}, 100);
}())
</script>
</body>
</html>`;
}
复制代码
按照VS Code
官方的建议,出于安全性考虑,VS Code API
对象(上文代码里的vscode
变量)应该是私有的,不要设置为全局变量。
安全性
在webview
中编写代码有一些安全性的最佳实践,需要开发者知晓
限制权限
webview
应该仅收紧对各项能力权限的限制,比如说你的webview
程序不需要运行js
脚本,那么就不要设置enableScripts: true
;如果你的程序不需要加载本地资源,那么就不要设置localResourceRoots
开启对本地的访问权限
内容安全政策
内容安全政策 更进一步的限制哪些内容可以在webview
中被加载和执行,例如一个内容安全政策可以限制只有某个白名单内的脚本才能可以在webview
中运行,或者告知webview
只可以加载https
协议的图片。
增加内容安全政策的方法是,在webview
的head
标签顶端增加<meta http-equiv="Content-Security-Policy">
指令:
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
复制代码
政策default-src 'none';
不允许任何内容,在此基础上逐步构建更完善的政策,下面这段表示仅允许加载本地脚本和样式文件,且图片加载协议应该是https
的政策:
<meta
http-equiv="Content-Security-Policy"
content="default-src 'none'; img-src ${webview.cspSource} https:; script-src ${webview.cspSource}; style-src ${webview.cspSource};"
/>
复制代码
${webview.cspSource}
是一个占位符,关于这个变量的用法,可以参见 extension.ts#L196
从安全策略上讲,不允许内嵌的脚本或样式,将所有的脚本和样式改为外部文件的方式是一种很好的实践,这样我们就可以用csp
来控制其安全性
仅通过https加载资源
如果你的webview
允许加载资源,那么强烈推荐通过https
加载而非http
防止内容注入
一定要控制好用户的输入防止内容注入,例如以下的情况就需要额外注意:
- 文件内容
- 文件以及文件夹路径
- 用户设置
进阶
在通常的webview
生命周期里,webview
是被createWebviewPanel
函数所创建并在用户关闭或调用了dispose
方法时销毁,不过对于webview
的内容来说不是这样,tab
的切换会导致webview
的可见性来回切换,当webview
不可见时其内容会被销毁,当可见时内容又会被重新创建。例如上面示例中的计数器,当可见性在切换时其数据会因销毁而丢失
解决这个的最佳实践是确保你的webview
本身不存储状态,用消息机制来做webview
状态的保存和恢复
getState、setState
在webview
中可以使用getState
、setState
方法来解决数据的保存和恢复,保存的数据是JSON
格式。保存的数据在webview
切换为不可见状态、页面内容被销毁后依然可以保存,只有当webview
本身被销毁(关闭或执行dispose
)时才会销毁。
import * as vscode from 'vscode';
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,
{
enableScripts: true
}
);
panel.webview.html = getWebviewContent();
})
);
}
function getWebviewContent() {
return `
<!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="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
<h1 id="lines-of-code-counter">0</h1>
<script>
(function() {
// 获取VS Code API 对象
const vscode = acquireVsCodeApi();
const counter = document.getElementById('lines-of-code-counter');
const previousState = vscode.getState();
let count = previousState ? previousState.count : 0;
counter.textContent = count;
setInterval(() => {
counter.textContent = count++;
// Update the saved state
vscode.setState({ count });
}, 100);
}())
</script>
</body>
</html>`;
}
复制代码
getState
和setState
是保存状态的最佳的实践方式,性能开销要远低于
retainContextWhenHidden
序列化
通过WebviewPanelSerializer
,当VS Code
重启的时候你的webview
可以自动实现数据的恢复,其底层依赖getState
和setState
,并且只有当你为webview
注册WebviewPanelSerializer
的时候才会发挥作用。
如果要确保你的代码在webview
恢复可见后依然能够保持住之前的状态,首先你需要在package.json
的activation event
中增加一个onWebviewPanel
:
"activationEvents": [
...,
"onWebviewPanel:catCoding"
]
复制代码
这段代码确保了VS Code
在任何时候想要恢复webview
,插件都会被激活
然后在插件的activate
方法中,通过registerWebviewPanelSerializer
方法注册一个新的WebviewPanelSerializer
,这个WebviewPanelSerializer
就是负责存储webview
内容里的状态,这个状态是一个在webview
中调用setState
存储的JSON
格式的数据。
import * as vscode from 'vscode';
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,
{
enableScripts: true
}
);
panel.webview.html = getWebviewContent();
})
);
vscode.window.registerWebviewPanelSerializer('catCoding', new CatCodingSerializer());
}
class CatCodingSerializer implements vscode.WebviewPanelSerializer {
async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel, state: any) {
// `state` is the state persisted using `setState` inside the webview
console.log(`Got state: ${state}`);
// Restore the content of our webview.
//
// Make sure we hold on to the `webviewPanel` passed in here and
// also restore any event listeners we need on it.
webviewPanel.webview.html = getWebviewContent();
}
}
function getWebviewContent() {
return `
<!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="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
<h1 id="lines-of-code-counter">0</h1>
<script>
(function() {
// 获取VS Code API 对象
const vscode = acquireVsCodeApi();
const counter = document.getElementById('lines-of-code-counter');
const previousState = vscode.getState();
let count = previousState ? previousState.count : 0;
counter.textContent = count;
setInterval(() => {
counter.textContent = count++;
// Update the saved state
vscode.setState({ count });
}, 100);
}())
</script>
</body>
</html>`;
}
复制代码
现在如果重启打开一个webview
的VS Code
,我们会发现webview
中的state没有丢失
retainContextWhenHidden
对于UI极为复杂不能在恢复可见性后快恢复的webview
,我们可以retainContextWhenHidden
选项来代替getState
、setState
import * as vscode from 'vscode';
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,
{
enableScripts: true,
retainContextWhenHidden: true
}
);
panel.webview.html = getWebviewContent();
})
);
}
function getWebviewContent() {
return `
<!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="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
<h1 id="lines-of-code-counter">0</h1>
<script>
const counter = document.getElementById('lines-of-code-counter');
let count = 0;
setInterval(() => {
counter.textContent = count++;
}, 100);
</script>
</body>
</html>`;
}
复制代码
这个方式对内存性能损耗较大,尽量不要使用
按照
VS Code
官方文档的说法,当webview
页面不可见时脚本将会本挂起,当页面恢复可见性时脚本叫被唤醒,但在1.57.0
版本的VS Code
下,似乎页面不可见的时候计数器也没有停止,这点需要注意。