VSCode插件-WebView
# 0.前言
本篇博客记录的是 VSCode 插件的 WebView 模块。
记录 webview 常用操作及解决方案如下:
webview的基础创建,以及常见Opition说明。- 更新页面内容。
- 通过生命周期函数可以实现对
webview页面事件的捕获,如:colorTheme、销毁事件等。 - 本地资源的加载方案*
Extension与WebView的通信*- 当
VSCode被异常关闭后,重新打开后如何维持原有的webview状态。
# 1.实战技巧
# 1.1. 如何创建一个WebView,并使其支持 js 功能
// Create and show a new webview
const panel = vscode.window.createWebviewPanel(
'catCoding', // 标识 webview 的type (在内部使用)
'Cat Coding', // Tabs 的 Title
vscode.ViewColumn.One, // 指定展示的区域(如,第1个Col)
{ enableScripts: true } // Webview 的 options
);
2
3
4
5
6
7
注意,此时这一步只是创建了面板,需要指定html显示页面,在这里使用字符串的形式导入文件(因为在VSCode插件中引入外部文件会受到安全限制的影响,后面会单独讲解)。
// 将 html 挂载到 panel 面板
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>`;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
html默认无法执行js代码,需要在createWebviewPanel打开enableScripts属性。
# 1.2.更新页面内容
非常简单,对panel.webview.html 重新赋值 html 字符串。
这里产生一个疑问,这里存在性能问题吧,
VScode给的例子是一个不断变化的猫的图片,每次变化都需要重新生成这段代码,而且也没有diff操作。
const updateWebview = () => {
panel.title = cat;
panel.webview.html = getWebviewContent(cat);
};
2
3
4
# 1.3.面板生命周期:销毁|隐藏
使用 createWebviewPanel 创建 panel 后,我们可以捕获该 panel 状态:
销毁(
dispose):可以通过panel.onDidpose生命周期捕获。隐藏(
Visibility)和移动(Moving):都可以通过onDidChangeViewState生命周期捕获。- 对于隐藏状态,可以查看
panel.visiblity - 对于隐藏状态的
webveiw,可以调用panel.reveal()重新唤醒 webview是否被销毁过,可以查看panel._store._isDisposed。
更多的属性查看,请通过断点
Debug调试。- 对于隐藏状态,可以查看
使用 onDidDispose 实现 WebView的 重新激活
let currentPanel = vscode.WebviewPanel | undefined // 初始化
const columnToShowIn = vscode.window.activeTextEditor
? vscode.window.activeTextEditor.viewColumn
: undefined;
if (currentPanel) {
// 如果已存在变量,则重新激活
currentPanel.reveal()
// currentPanel.reveal(columnToShowIn); // reveal 还行可以指定激活的区域
} else {
// create a new panel
currentPanel = vscode.window.createWebviewPanel(
'webviewID', // 标识 webview 的 ID, 如重启VSCOde后维护窗体时需要此字段。
'Cat Coding', // webview 的 tab title
columnToShowIn,
{
enableScripts: true,
retainContextWhenHidden: true
}
);
// 渲染 猫 的页面
currentPanel.webview.html = getWebviewContent('Coding Cat');
// 关闭 webView 的时候清空实例对象。
currentPanel.onDidDispose(
() => {
currentPanel = undefined;
},
null,
context.subscriptions
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
使用onDidChangeViewState 更新 webview
panel.onDidChangeViewState(
e => {
// 通过 e.webviewPanel
const panel = e.webviewPanel;
.....
},
null,
context.subscriptions
);
2
3
4
5
6
7
8
9
结合switch case语法
panel.onDidChangeViewState(
e => {
const panel = e.webviewPanel;
// 不同的 区域 显示不同的 猫咪 图片
switch (panel.viewColumn) {
case vscode.ViewColumn.One:
updateWebviewForCat(panel, 'Coding Cat');
return;
case vscode.ViewColumn.Two:
updateWebviewForCat(panel, 'Compiling Cat');
return;
case vscode.ViewColumn.Three:
updateWebviewForCat(panel, 'Testing Cat');
return;
}
},
null,
context.subscriptions
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 1.4.本地资源的加载
VScode 插件不允许直接加载本地资源,意味着 panel.webview.html 内部所有的外部资源都需要重新处理。
处理步骤如下:
拼接绝对路径地址
import * as path from "vscode" path.join(context.extensionPath, 'media', 'cat.gif') // 推荐使用 path.resolve()1
2
3添加
file://文件协议头// DiskPath 磁盘路径 const onDiskPath = vscode.Uri.file( path.join(context.extensionPath, 'media', 'cat.gif') );1
2
3
4
转化为
VScode的协议头:vscode-resource:/const catGifSrc = panel.webview.asWebviewUri(onDiskPath); // vscode-resource:/Users/toonces/projects/vscode-cat-coding/media/cat.gif1
2
最后一步,将生成的新协议的地址插入到
html中,重构html页面。<img src="${catGifSrc}" width="300" />1
控制资源的访问权限
上面的例子中,是将位于静态文件夹下(./media/cat.svg)猫的图片插入到 html 中,我们还可以限制资源访问的文件夹,如只允许读取 media 中的图片资源。
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{
// 只允许读取 `media` 中的图片资源。
localResourceRoots: [vscode.Uri.file(path.join(context.extensionPath, 'media'))]
}
);
2
3
4
5
6
7
8
9
如果不允许读取所有本地资源,可以直接将 localResourceRoots 设置为[]
# 1.5.Extension 与 WebView 的通信
情况1:Extensions WebView
Extensions往WebView传递信息可以通过:webview.postMessage({...}),如传递command
// 注册一个新的 command
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.doRefactor', () => {
if (!currentPanel) {
return;
}
// 可以传递所以可以被`JSON`序列化的内容
currentPanel.webview.postMessage({ command: 'refactor' });
})
2
3
4
5
6
7
8
9
WebView接受信息,即,让HTML 接受这个 Message
方式:全局监听 message 事件。
<script>
// 在 HTML 内部的 sript 监听 `message` 事件
window.addEventListener('message', event => {
const message = event.data; // event.data 是一个 JSON 对象
switch (message.command) {
case 'refactor':
.....
break;
}
});
</script>
2
3
4
5
6
7
8
9
10
11
注:为了让
HTML支持JS代码,在创建webview时,设置{enableSripte: true}
情况2:WebView Extensions
在 HTML 的 <script> 有一个内置的函数acquireVSCodeApi,可以得到vscode对象,通过调用 vscode.postMessage 函数向Extensions传递。
<script>
(function() {
const vscode = acquireVsCodeApi();
if (Math.random() < 0.001 * count) {
vscode.postMessage({
command: 'alert',
text: '🐛 on line ' + count
})
}
}, 100);
}())
</script>
2
3
4
5
6
7
8
9
10
11
12
Extensions 通过onDidReceiveMessage 钩子函数获取信息:
// Extensions 接受 Message
panel.webview.onDidReceiveMessage(
message => {
switch (message.command) {
case 'alert':
vscode.window.showErrorMessage(message.text);
return;
}
},
undefined,
context.subscriptions
);
2
3
4
5
6
7
8
9
10
11
12
总结:
Extensions发:panel.webview.postMessage()Extensions收:panel.webview.onDidReceiveMessage()Webview发:vscode.postMessage()(vscode通过内置的acquireVsCodeApi获取)Webview收:windows.addEventListener("message",event=>{})
# 1.6.状态持久化(Persistence)
首先明确有哪儿些状态需要被缓存:
webview窗体本身:easy,只需要记忆panel对象即可。注:该操作需要维持一个全局的
panel变量用于保存窗体对象,如果丢失了窗体对象,就无法重新获取。webview窗体被隐藏(panel.visible)或者被销毁(panel.dispose)时,webview嵌入的html内部所有的状态会被丢失。整个
VSCode软件退出,重启后,需要重新显示webview内容。
# 方案1:维护窗体本身
维护一个全局变量
let panel = undefined // 初始化
function activate(context) {
if (panel) {
// 如果存在 panel 变量,重新激活窗体
panel.reveal();
}else{
// panel undefined时,创建一个新的窗体
panel = vscode.window.createWebviewPanel(
"webview_type",
"Tabs Title",
1, // 显示的 Col
{ enableSript: true} // 各种 Options
);
}
// 关闭时,销毁 panel 变量
panel.onDidDispose(()=>{ panel = undefined},null,context.subscriptions)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 方案2:窗口隐藏时,仍维持状态
这简单的方案,在创建 webview 时设置options
panel = vscode.window.createWebviewPanel(
"webview_type",
"Tabs Title",
1,
{
enableSript: true,
retainContextWhenHidden: true
}
);
2
3
4
5
6
7
8
9
设置 retainContextWhenHidden 后就无法再与 hidden窗体进行通信。
# 方案3:vscode重启后,维护窗体。
在创建 webview 时,一直不理解第一个string 标识,原文中称该变量为webview type,用于内部对窗体的标识(Used internally),具体实践步骤如下:
创建新
webview时,指定webview typeconst panel = vscode.window.createWebviewPanel("webview_id","Tabs Title",1,{})1指定
vscode重启后,触发的窗体vscode.window.registerWebviewPanelSerializer( 'webview_type', new RetainContextSerializer() );1
2
3
4当
VSCode重启后,将会调用由RetainContextSerializer构造的窗体。上述构造函数中的内容,
重启后需要显示的页面:
showCatAndCount()通过
webviewPanel可以拿到restore前的面板对象,重新赋值上页面内容即可。class RetainContextSerializer { async deserializeWebviewPanel(webviewPanel, state) { webviewPanel.webview.html = showCatAndCount(); } }1
2
3
4
5最后需要在
package.json中设置"activationEvents": [ ..., "onWebviewPanel:catCoding" ]1
2
3
4