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.gif
1
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 type
const 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