VSCode插件开发全攻略(七)WebView
更多文章请戳VSCode插件开发全攻略系列目录导航。
什么是Webview
大家都知道,整个VSCode编辑器就是一张大的网页,其实,我们还可以在
Visual Studio Code中创建完全自定义的、可以间接和
nodejs通信的特殊网页(通过一个
acquireVsCodeApi特殊方法),这个网页就叫
WebView。内置的
Markdown的预览就是使用
WebView实现的。使用
Webview可以构建复杂的、支持本地文件操作的用户界面。
VSCode插件的WebView类似于iframe的实现,但并不是真正的iframe(我猜底层应该还是基于iframe实现的,只不过上层包装了一层),通过开发者工具可以看到:
demo
在我们的vscode-plugin-demo中,我写了一个非常简单、没啥实际意义的
Webview示例仅供参考,在任意编辑器右键可以看到
打开Webview的菜单:
什么时候适合使用WebView
虽然Webview令人很振奋,因为基于它我们可以随意发挥不受限制,但必须注意还是要慎用,毕竟VSCode是很注重性能的,不能因为你一个插件拖累了整个IDE,一般仅在原有API和功能以及交互方式无法满足你时才需要考虑,另外,设计糟糕的Webview也很容易在
VS Code中让人感觉不舒适,不能让人家一看就觉得你这是一张网页,好看的UI也很重要。
这是官网给出的建议,在使用webview之前请考虑以下事项:
- 这个功能真的需要放在
VSCode
中吗?作为单独的应用程序或网站会不会更好呢? - webview是实现这个功能的唯一方法吗?可以使用常规VS Code API吗?
- 您的webview是否会带来足够的用户价值以证明其高资源成本?
正式开始WebView之旅
创建WebView
context.subscriptions.push(vscode.commands.registerCommand('extension.demo.openWebview', function (uri) { // 创建webview const panel = vscode.window.createWebviewPanel( 'testWebview', // viewType "WebView演示", // 视图标题 vscode.ViewColumn.One, // 显示在编辑器的哪个部位 { enableScripts: true, // 启用JS,默认禁用 retainContextWhenHidden: true, // webview被隐藏时保持状态,避免被重置 } ); panel.webview.html = `<html><body>你好,我是Webview</body></html>`
几点说明:
- 默认情况下,在Web视图中禁用
JavaScript
,但可以通过传入enableScripts: true
选项轻松启用; - 默认情况下当webview被隐藏时资源会被销毁,通过
retainContextWhenHidden: true
会一直保存,但会占用较大内存开销,仅在需要时开启;
加载本地资源
出于安全考虑,Webview默认无法直接访问本地资源,它在一个孤立的上下文中运行,想要加载本地图片、js、css等必须通过特殊的
vscode-resource:协议,网页里面所有的静态资源都要转换成这种格式,否则无法被正常加载。
vscode-resource:协议类似于
file:协议,但它只允许访问特定的本地文件。和
file:一样,
vscode-resource:从磁盘加载绝对路径的资源。
我简单封装了一个转换方法:
/** * 获取某个扩展文件相对于webview需要的一种特殊路径格式 * 形如:vscode-resource:/Users/toonces/projects/vscode-cat-coding/media/cat.gif * @param context 上下文 * @param relativePath 扩展中某个文件相对于根目录的路径,如 images/test.jpg */ getExtensionFileVscodeResource: function(context, relativePath) { const diskPath = vscode.Uri.file(path.join(context.extensionPath, relativePath)); return diskPath.with({ scheme: 'vscode-resource' }).toString(); }
默认情况下,
vscode-resource:只能访问以下位置中的资源:
- 扩展程序安装目录中的文件。
- 用户当前活动的工作区内。
- 当然,你还可以使用
dataURI
直接在Webview中嵌入资源,这种方式没有限制;
从文件加载HTML内容
默认不支持从文件加载HTML,需要自己封装代码,我简单封装了一个供大家参考:
/** * 从某个HTML文件读取能被Webview加载的HTML内容 * @param {*} context 上下文 * @param {*} templatePath 相对于插件根目录的html文件相对路径 */ function getWebViewContent(context, templatePath) { const resourcePath = path.join(context.extensionPath, templatePath); const dirPath = path.dirname(resourcePath); let html = fs.readFileSync(resourcePath, 'utf-8'); // vscode不支持直接加载本地资源,需要替换成其专有路径格式,这里只是简单的将样式和JS的路径替换 html = html.replace(/(<link.+?href="|<script.+?src="|<img.+?src=")(.+?)"/g, (m, $1, $2) => { return $1 + vscode.Uri.file(path.resolve(dirPath, $2)).with({ scheme: 'vscode-resource' }).toString() + '"'; }); return html; }
运行这段代码之后,会自动将HTML文件中
link、
href、
script、
img的资源相对路径全部替换成正确的
vscode-resource:绝对路径,例如:
../../lib/vue-2.5.17/vue.js 变成 vscode-resource:/Users/test/workspace/vscode-plugin-demo/lib/vue-2.5.17/vue.js
使用方法如下:
panel.webview.html = getWebViewContent(context, 'src/view/test-webview.html');
消息通信
重头戏来了,
Webview和普通网页非常类似,不能直接调用任何
VSCodeAPI,但是,它唯一特别之处就在于多了一个名叫
acquireVsCodeApi的方法,执行这个方法会返回一个超级阉割版的
vscode对象,这个对象里面有且仅有如下3个可以和插件通信的API:
插件和
Webview之间如何互相通信呢?
插件给
Webview发送消息(支持发送任意可以被
JSON化的数据):
panel.webview.postMessage({text: '你好,我是小茗同学!'});
Webview端接收:
window.addEventListener('message', event => { const message = event.data; console.log('Webview接收到的消息:', message); }
Webview主动发送消息给插件:
vscode.postMessage({text: '你好,我是Webview啊!'});
插件接收:
panel.webview.onDidReceiveMessage(message => { console.log('插件收到的消息:', message); }, undefined, context.subscriptions);
简单通信封装
为了双方通信方便,我把它们简单封装了一下,仅供参考,Webview端:
const callbacks = {}; // 存放所有的回调函数 /** * 调用vscode原生api * @param data 可以是类似 {cmd: 'xxx', param1: 'xxx'},也可以直接是 cmd 字符串 * @param cb 可选的回调函数 */ function callVscode(data, cb) { if (typeof data === 'string') { data = { cmd: data }; } if (cb) { // 时间戳加上5位随机数 const cbid = Date.now() + '' + Math.round(Math.random() * 100000); // 将回调函数分配一个随机cbid然后存起来,后续需要执行的时候再捞起来 callbacks[cbid] = cb; data.cbid = cbid; } vscode.postMessage(data); } window.addEventListener('message', event => { const message = event.data; switch (message.cmd) { // 来自vscode的回调 case 'vscodeCallback': console.log(message.data); (callbacks[message.cbid] || function () { })(message.data); delete callbacks[message.cbid]; // 执行完回调删除 break; default: break; } });
插件端:
let global = { projectPath, panel}; panel.webview.onDidReceiveMessage(message => { if (messageHandler[message.cmd]) { // cmd表示要执行的方法名称 messageHandler[message.cmd](global, message); } else { util.showError(`未找到名为 ${message.cmd} 的方法!`); } }, undefined, context.subscriptions); /** * 存放所有消息回调函数,根据 message.cmd 来决定调用哪个方法, * 想调用什么方法,就在这里写一个和cmd同名的方法实现即可 */ const messageHandler = { // 弹出提示 alert(global, message) { util.showInfo(message.info); }, // 显示错误提示 error(global, message) { util.showError(message.info); }, // 回调示例:获取工程名 getProjectName(global, message) { invokeCallback(global.panel, message, util.getProjectName(global.projectPath)); } } /** * 执行回调函数 * @param {*} panel * @param {*} message * @param {*} resp */ function invokeCallback(panel, message, resp) { console.log('回调消息:', resp); // 错误码在400-600之间的,默认弹出错误提示 if (typeof resp == 'object' && resp.code && resp.code >= 400 && resp.code < 600) { util.showError(resp.message || '发生未知错误!'); } panel.webview.postMessage({cmd: 'vscodeCallback', cbid: message.cbid, data: resp}); }
按上述方法封装之后,例如,Webview端想要执行名为
openFileInVscode命令只需要这样:
callVscode({cmd: 'openFileInVscode', path: `package.json`}, (message) => { this.alert(message); });
然后在插件端的
messageHandler实现
openFileInVscode方法即可,其它都不用管:
const messageHandler = { // 省略其它方法 openFileInVscode(global, message) { util.openFileInVscode(`${global.projectPath}/${message.path}`); invokeCallback(global.panel, message, '打开文件成功!'); } };
以上封装的比较随便,只是给大家提供一个思路,有时间可以好好封装一下。
主题适配
Webview可以根据
VS Code的当前主题更改其外观,原理是body上面添加当前主题名称,主要有以下三种:
vscode-light
- 浅色主题;vscode-dark
-深色主题;vscode-high-contrast
- 高对比度主题;
所以我们可以通过自己写样式来适配不同主题:
/* 浅色主题 */ .body.vscode-light { background: white; color: black; } /* 深色主题 */ body.vscode-dark { background: #252526; color: white; } /* 高对比度主题 */ body.vscode-high-contrast { background: white; color: red; }
深色主题效果:
生命周期
webview由创建它的扩展程序所有,返回的
panel对象你必须自己保存,如果你的扩展程序丢失了这个引用,那么将无法再次重新访问该
webview,即使Web视图继续显示在
vscode中。
用户也可以随时关闭
webview面板。当用户关闭webview面板时,webview本身将被销毁,此时不能再使用panel引用,否则将会出现异常,可以通过监听
onDidDispose事件在这里面做一些销毁操作。
可以通过
panel.dispose()方法主动关闭webview。
状态保持
当webview移动到后台又再次显示时,webview中的任何状态都将丢失。
解决此问题的最佳方法是使你的webview无状态,通过消息传递来保存webview的状态。
state
在webview的js中我们可以使用
vscode.getState()和
vscode.setState()方法来保存和恢复JSON可序列化状态对象。当webview被隐藏时,即使webview内容本身被破坏,这些状态仍然会保存。当然了,当webview被销毁时,状态将被销毁。
序列化
通过注册
WebviewPanelSerializer可以实现在
VScode重启后自动恢复你的
webview,当然,序列化其实也是建立在
getState和
setState之上的。
注册方法:
vscode.window.registerWebviewPanelSerializer
retainContextWhenHidden
对于具有非常复杂的UI或状态且无法快速保存和恢复的
webview,我们可以直接使用
retainContextWhenHidden选项。设置
retainContextWhenHidden: true后即使webview被隐藏到后台其状态也不会丢失。
尽管
retainContextWhenHidden很有吸引力,但它需要很高的内存开销,一般建议在实在没办法的时候才启用。
getState和
setState是持久化的首选方式,因为它们的性能开销要比
retainContextWhenHidden低得多。
调试
注意,要调试Webview不能直接把VSCode的开发者工具打开,直接打开就会和我们最前面的截图看到的那样,你只能看到一个
<webview></webview>标签,看不到代码,要看代码需要按下
Ctrl+Shift+P然后执行
打开Webview开发工具,英文版应该是
Open Webview Developer Tools:
审查Webview:
这个时候需要特别注意错误日志出现的位置,如果是Webview的错误,一般打印在前面说的这个开发者工具,但如果是插件端的错误只会打印在整个VSCode的开发者工具里。
糟糕,距离最开始接触
Webview已经有一段时间了,本来有挺多想写的,但是现在居然没灵感了,额……坑爹啊
参考链接
https://code.visualstudio.com/docs/extensions/webview
- VSCode插件开发全攻略(一)概览
- VSCode插件开发全攻略(二)HelloWord
- VSCode插件开发全攻略(三)package.json详解
- VSCode插件开发全攻略(四)命令、菜单、快捷键
- VSCode插件开发全攻略(五)跳转到定义、自动补全、悬停提示
- VSCode插件开发全攻略(十)打包、发布、升级
- VSCode插件开发全攻略(六)开发调试技巧
- VSCode插件开发全攻略(九)常用API总结
- vsCode 开发微信小程序插件
- vsCode 【推荐】微软旗下最好用的前端开发IDE编辑器 常用插件
- VS Code v.s Atom-IDE:Web 开发哪家强?
- xmake-vscode插件开发过程记录
- xmake-vscode插件开发过程记录
- [Phonegap+Sencha Touch] 移动开发77 Cordova Hot Code Push插件实现自己主动更新App的Web内容
- VSCode 必装的 10 个高效开发插件
- VSCode拓展插件推荐(HTML、Node、Vue、React开发均适用)
- [Phonegap+Sencha Touch] 移动开发77 Cordova Hot Code Push插件实现自动更新App的Web内容
- vscode 插件开发入门
- 作为JavaScript开发人员,这些必备的VS Code插件你都用过吗?