如何在 VSCode 扩展中使用 WebView
开发 VSCode 扩展,有时候 VSCode 本身的交互无法满足需要,这时候就可以使用 WebView 来实现自定义的交互。本文将介绍如何在 VSCode 扩展中使用 WebView。VSCode 目前最新版本已经可以在 Editor,SideBar,Panel 中使用 WebView。基本覆盖了 VSCode 的主要 UI 部分。
1. 创建 WebView
在 Editor 和 SideBar / Panel 中创建 WebView 的方法是不同的。在 Editor 中,可以使用 vscode.window.createWebviewPanel
方法,而在 SideBar / Panel 中,需要使用 vscode.window.registerWebviewViewProvider
方法。
1.1 Editor 中创建 WebView
在 vscode.window.createWebviewPanel
方法中,有四个参数:
viewType
:WebView 的类型,用于区分不同的 WebView。title
:WebView 的标题。viewColumn
:WebView 的位置,可以是vscode.ViewColumn
枚举值。options
:WebView 的配置,可以设置 WebView 的enableScripts
,retainContextWhenHidden
等属性。
const panel = vscode.window.createWebviewPanel(
'webviewSample',
'WebView 示例',
vscode.ViewColumn.Beside,
{
enableScripts: true, // 是否启用脚本
localResourceRoots: [vscode.Uri.file(path.join(context.extensionPath))] // 本地资源根目录
}
);
创建后,可以通过 panel.webview
获取 WebView 的实例,然后可以通过 panel.webview.html
设置 WebView 的内容。
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>WebView 示例</title>
</head>
<body>
<h1>WebView 示例</h1>
</body>
</html>
`;
这样,就可以在 Editor 中创建一个 WebView 了。可以把这部分代码放在一个 Command 中(Command 需要注册与定义),然后通过 Command Palette 执行。
vscode.commands.registerCommand('webviewSample.show', () => {
const panel = vscode.window.createWebviewPanel(
'webviewSample',
'WebView 示例',
vscode.ViewColumn.Beside,
{
enableScripts: true,
localResourceRoots: [vscode.Uri.file(path.join(context.extensionPath))]
}
);
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>WebView Sample</title>
</head>
<body>
<h1>Hello WebView in Editor</h1>
</body>
</html>
`;
});
这样,按下 Ctrl + Shift + P
在 Command Palette 中执行 webviewSample.show
命令,打开一个 WebView。
1.2 SideBar / Panel 中创建 WebView
在 SideBar / Panel 中创建 WebView,则是使用 vscode.window.registerWebviewViewProvider
方法。这个方法有两个参数:
viewType
:WebView 的 Id,用于区分不同的 WebView。provider
:一个vscode.WebviewViewProvider
对象,主要是实现了resolveWebviewView
方法。
因此,你需要先实现一个 WebviewViewProvider
:
class WebviewSampleProvider implements vscode.WebviewViewProvider {
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>WebView 示例</title>
</head>
<body>
<h1>Hello WebView Provider</h1>
</body>
</html>
`;
}
}
然后,注册这个 Provider:
vscode.window.registerWebviewViewProvider('webviewSample', new WebviewSampleProvider());
接着,还需要去 Package.json 中配置 contributes.viewsContainers
,添加一个视图容器:
"contributes": {
"viewsContainers": {
"activitybar": [
{
"id": "webviewSidebar",
"title": "WebView Sidebar",
"icon": "webviewSample.svg" // 建议使用无色 svg 图标
}
]
}
}
activitybar
是 SideBar 的视图容器,这里创建了一个叫 webviewSidebar
的视图容器。接着,我们再去 contributes.views
中添加一个视图:
"contributes": {
"views": {
"webviewSidebar": [
{
"id": "webviewSample", // 此处的 id 和 Webview 注册时填写的 viewType 一致
"name": "WebView Sample",
"type": "webview" // 视图类型为 webview
}
]
}
}
这样启动扩展,就可以在 SideBar 中看到一个 webviewSample.svg
的图标了,点击即可加载 webviewSample
的 Webview。如果是要在 Panel 中创建 WebView,只需要把 activitybar
改成 panel
即可。值得注意的是,视图容器的的类型是数组,也就是一个容器可以包含多个视图。在 Sidebar 中,多个视图会使用纵向排列,而在 Panel 中,多个视图会使用横向排列。多个视图的话,视图是可以折叠的,默认为展开,如果需要初始化时为折叠,则可以通过配置 visibility
为 collapsed
来折叠。
2. WebView 通信
WebView 和 VSCode 之间的通信,可以通过 postMessage
和 onMessage
方法来实现。WebView 可以通过 postMessage
向 VSCode 发送消息,VSCode 可以通过 onMessage
监听 WebView 发送的消息。
每个 WebView 都有一个 acquireVsCodeApi
方法,可以获取一个 vscode
对象,这个对象有一个 postMessage
方法,可以向 VSCode 发送消息。
const vscode = acquireVsCodeApi();
vscode.postMessage({ command: 'alert', text: 'Hello VSCode!' });
在 VSCode 中,可以通过 webview
对象的 onDidReceiveMessage
方法监听 WebView 发送的消息。webview
对象分别在 WebviewPanel
和 WebviewView
中取得。
webview.onDidReceiveMessage(message => {
switch (message.command) {
case 'alert':
vscode.window.showInformationMessage(message.text);
break;
}
});
而在 vscode 的 extension Host 中,则可以通过 webview
对象的 postMessage
方法向 WebView 发送消息。
webview.postMessage({ command: 'alert', text: 'Hello WebView!' });
在 WebView 中,可以通过 window.addEventListener
方法监听 message
事件,来接收 VSCode 发送的消息。
window.addEventListener('message', event => {
const message = event.data;
switch (message.command) {
case 'alert':
alert(message.text);
break;
}
});
这样,就可以实现 WebView 和 VSCode 之间的通信了。
此外,vscode
对象不仅有 postMessage
方法,还有 getState
和 setState
方法,可以用来保存 WebView 的状态。这样,就可以在 WebView 的生命周期中保存一些状态了。
3. WebView 生命周期
WebView 的生命周期主要有两个事件:onDidDispose
和 onDidChangeViewState
。onDidDispose
会在 WebView 被销毁时触发,onDidChangeViewState
会在 WebView 的可见性发生变化时触发。
panel.onDidDispose(() => {
// WebView 被销毁时触发
});
panel.onDidChangeViewState(() => {
// WebView 的可见性发生变化时触发
});
在 WebviewViewProvider
中,也有类似的生命周期事件:
class WebviewSampleProvider implements vscode.WebviewViewProvider {
resolveWebviewView(webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext, token: vscode.CancellationToken) {
webviewView.onDidDispose(() => {
// WebView 被销毁时触发
});
webviewView.onDidChangeVisibility(() => {
// WebView 的可见性发生变化时触发
});
}
}
这样,就可以在 WebView 的生命周期中做一些操作了。
如果希望 WebView 在隐藏时不被销毁,可以在创建 WebView 时设置 retainContextWhenHidden
为 true
。SideBar / Panel 中的 WebView 默认是保持上下文的,因此无需设置。
4. WebView 中的资源
WebView 中可以加载本地资源,但是需要通过 localResourceRoots
属性来指定本地资源的根目录。这样,WebView 才能加载本地资源。
const panel = vscode.window.createWebviewPanel(
'webviewSample',
'WebView 示例',
vscode.ViewColumn.Beside,
{
enableScripts: true,
localResourceRoots: [vscode.Uri.file(path.join(context.extensionPath))]
}
);
然后,可以通过 webview.asWebviewUri
方法来获取本地资源的 Uri。
const scriptUri = panel.webview.asWebviewUri(vscode.Uri.file(path.join(context.extensionPath, 'script.js')));
这样,就可以获得一个本地资源的 Uri 了。然后,可以在 WebView 中通过这个 Uri 来加载本地资源。
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>WebView 示例</title>
<script src="${scriptUri}"></script>
</head>
<body>
<h1>Hello WebView Provider</h1>
</body>
</html>
`;
要注意的是,WebView 中加载的资源,需要遵循 CSP(Content Security Policy)规则。因此,需要在 WebView 的 contentSecurityPolicy
属性中设置 CSP 规则。
const panel = vscode.window.createWebviewPanel(
'webviewSample',
'WebView 示例',
vscode.ViewColumn.Beside,
{
enableScripts: true,
localResourceRoots: [vscode.Uri.file(path.join(context.extensionPath)],
contentSecurityPolicy: 'default-src \'none\'; script-src vscode-resource: \'unsafe-inline\'; style-src vscode-resource: \'unsafe-inline\';'
}
);
这样,就可以在 WebView 中加载本地资源了。
5. Web 前端框架支持与调试
前面说了那么多,都只是原生的 Web 技术。如果想要在 WebView 中使用 React,Vue 等前端框架,需要注意一些问题。
使用前端框架,我们并没有办法等做好页面,打包好,然后再与 VSCode 的 Host 代码联调,因此就需要通过某种方法来让 WebView 加载本地服务。
因此,我们可以引入一个中间层,这个中间层是一个静态网页,然后通过 iframe 的方式来加载本地服务。
<!DOCTYPE html>
<html>
<head>
<base href="">
<style>
iframe {
width: 100%;
border: none;
height: 99.5vh;
}
body {
overflow: hidden;
}
</style>
</head>
<body style="padding: 0;">
<iframe id="main" src="http://localhost:5173"
frameborder="0"
sandbox="allow-same-origin allow-pointer-lock allow-scripts allow-downloads allow-forms allow-popups"
allow="clipboard-read; clipboard-write;"></iframe>
<script src="js/main.js"></script>
<body>
</html>
// main.js
// 获取 iframe 的 Vite 本地服务页面的 window 对象
let main = document.getElementById('main').contentWindow;
const vscode = acquireVsCodeApi();
document.getElementById('main').onload = () => {
// 待页面加载完成后,将 vscode 的默认样式转发给 iframe 内页面
main.postMessage({ command: 'style', data: document.querySelector('html').getAttribute('style')}, '*');
};
// 消息通信转发
window.addEventListener('message', event => {
const message = event.data;
switch (message.command) {
case 'forward':
{
// 转发 iframe 的消息给扩展后端
message.command = message.real;
vscode.postMessage(message);
break;
}
default:
{
// 转发扩展后端的消息给 iframe
main.postMessage(message, '*');
break;
}
}
});
这个中间层做了三件事:
- 通过 iframe 加载本地服务。
- 将 vscode 的默认样式转发给 iframe 内页面。
- 将消息通信转发给 iframe 内页面,以及将 iframe 内页面的消息转发给扩展 Host。
而框架网页,则需要再全局加上一句:window.vscode = window.acquireVsCodeApi ? window.acquireVsCodeApi() : window.parent;
,这样就可以在框架中使用 vscode
对象了。
此外,还需要接收 style
消息,将 vscode 的默认样式应用到框架网页中。
window.addEventListener('message', (event) => {
const message = event.data;
switch (message.command) {
case 'style': {
// 接受 iframe 父窗体转发的 VSCode html 注入的 style
document.querySelector('html')!.setAttribute('style', message.data);
break;
}
}
});
这样,调试时,我们可以先启动本地服务,然后再启动 VSCode 扩展,这样就可以在 WebView 中调试 React,Vue 等前端框架了。而打包时,只需要将打包输出目录配置为扩展的打包目录,然后在 .vscodeignore
中配置中间层的目录忽略打包即可。
因此,在返回 Webview 的 html 时,可以这样写:
// 通过 dev 文件夹是否存在来判断现在是打包模式还是开发模式
let exists = fs.existsSync(path.resolve(__dirname, '..', '..', 'dev'));
// 获取 index.html 文件路径
let mainHtml = exists ?
path.resolve(__dirname, '..', '..', 'dev', 'index.html') :
path.resolve(__dirname, '..', 'webview', 'index.html');
const root = this.context.extensionPath;
// 获取 base 路径的 VSCode uri,这样才能载入本地资源
let baseUrl = vscode.Uri.file(exists ?
path.join(root, 'dev', '/') :
path.join(root, 'out', 'webview', '/')
);
// 读取到的文件做一些处理,替换 base 路径,添加 CSP (才能正常执行外部的 js)等。
return fs.readFileSync(mainHtml).toString().replace(/<base href="[^"]*">/,
`<base href="${this.webview!.asWebviewUri(baseUrl)}">`)
.replace(/<(script|link) /g, '<$1 nonce="vuescript" ')
.replace(/<head>/, `<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'none';
img-src *; font-src http://* https://*; style-src http://* https://* 'unsafe-inline'; frame-src *;script-src 'nonce-vuescript';">`);
这里用了一个 nonce
来解决 CSP 的脚本加载问题,具体可以看这篇文章。
这样,就可以在 WebView 中使用调试 React,Vue 等前端框架的网页了。
6. 最后
可能看完文章你还是云里雾里,不过没关系,这里准备了一个 Vue3 + Element Plus
样例工程,你可以参考这个工程来快速编写一个使用 WebView 的 VSCode 扩展。
文章首发于:https://my.hancel.org/2024/04/04/how-to-use-webview-in-vscode-extension.html
膜拜大佬
传道者跳跳,我是你的提问人😋
一脸懵逼
tql
不愧是 jump jump big old
不愧是 jump jump big old
不愧是jump jump big old
不愧是jump jump big old
不愧是 jump jump big old
不愧是jump jump big old
不愧是 jump jump big old
不愧是 jump jump big old