更多文章请戳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文件中linkhrefscriptimg的资源相对路径全部替换成正确的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,当然,序列化其实也是建立在getStatesetState之上的。

注册方法:vscode.window.registerWebviewPanelSerializer

retainContextWhenHidden

对于具有非常复杂的UI或状态且无法快速保存和恢复的webview,我们可以直接使用retainContextWhenHidden选项。设置retainContextWhenHidden: true后即使webview被隐藏到后台其状态也不会丢失。

尽管retainContextWhenHidden很有吸引力,但它需要很高的内存开销,一般建议在实在没办法的时候才启用。

getStatesetState是持久化的首选方式,因为它们的性能开销要比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插件开发全攻略(七)WebView的更多相关文章

  1. VSCode插件开发全攻略(一)概览

    文章索引 VSCode插件开发全攻略(一)概览 VSCode插件开发全攻略(二)HelloWord VSCode插件开发全攻略(三)package.json详解 VSCode插件开发全攻略(四)命令. ...

  2. VSCode插件开发全攻略(八)代码片段、设置、自定义欢迎页

    更多文章请戳VSCode插件开发全攻略系列目录导航. 代码片段 代码片段,也叫snippets,相信大家都不陌生,就是输入一个很简单的单词然后一回车带出来很多代码.平时大家也可以直接在vscode中创 ...

  3. VSCode插件开发全攻略(六)开发调试技巧

    更多文章请戳VSCode插件开发全攻略系列目录导航. 前言 在介绍完一些比较简单的内容点之后,我觉得有必要先和大家介绍一些开发中遇到的一些细节问题以及技巧,特别是后面一章节将要介绍WebView的知识 ...

  4. VSCode插件开发全攻略(九)常用API总结

    更多文章请戳VSCode插件开发全攻略系列目录导航. 本文提炼一些常见的API使用场景供参考,本文内容有待完善. 编辑器相关 修改当前激活编辑器内容 替换当前编辑器全部内容: vscode.windo ...

  5. VSCode插件开发全攻略(十)打包、发布、升级

    更多文章请戳VSCode插件开发全攻略系列目录导航. 发布方式 插件开发完了,如何发布出去分享给他人呢?主要有3种方法: 方法一:直接把文件夹发给别人,让别人找到vscode的插件存放目录并放进去,然 ...

  6. VSCode插件开发全攻略(五)跳转到定义、自动补全、悬停提示

    更多文章请戳VSCode插件开发全攻略系列目录导航. 跳转到定义 跳转到定义其实很简单,通过vscode.languages.registerDefinitionProvider注册一个provide ...

  7. VSCode插件开发全攻略(四)命令、菜单、快捷键

    更多文章请戳VSCode插件开发全攻略系列目录导航. 命令 我们在前面HelloWord章节中已经提到了命令写法,这里再重温一下. context.subscriptions.push(vscode. ...

  8. VSCode插件开发全攻略(三)package.json详解

    更多文章请戳VSCode插件开发全攻略系列目录导航. package.json 在详细介绍vscode插件开发细节之前,这里我们先详细介绍一下vscode插件的package.json写法,但是建议先 ...

  9. VSCode插件开发全攻略(二)HelloWord

    更多文章请戳VSCode插件开发全攻略系列目录导航. 写着前面 学习一门新的语言或者生态首先肯定是从HelloWord开始. 您可以直接克隆我放在GitHub上vscode-plugin-demo 的 ...

随机推荐

  1. L2-028 秀恩爱分得快(模拟)

    古人云:秀恩爱,分得快. 互联网上每天都有大量人发布大量照片,我们通过分析这些照片,可以分析人与人之间的亲密度.如果一张照片上出现了 K 个人,这些人两两间的亲密度就被定义为 1/K.任意两个人如果同 ...

  2. Winform导入文件

    winfrom的选中文件的路径放进文本框,我还是一个新手,欢迎大家在评论里面多多指教

  3. stm32-arduino-f103

    希望给工作中偶尔要用的一些辅助板卡(例如运行信息现场记录)找一个快速开发的手段,Arduino作为流行的开源嵌入硬件框架,组件丰富,资料众多,所以想以Arduino作为平台.但是Arduino板子基本 ...

  4. python 代码求阶乘

    递归实现 1: #递归实现 def factorial(n): if n == 0: return 1 else: return n * factorial(n - 1)# 递归实现 递归实现 2: ...

  5. Ax2009中使用CLR发送邮件

    由于Ax2009系统方法SysMailer 发送中文的时候会乱码,一直找不到原因,用.NEt Framwork的类库可以解决中文乱码的问题.static void CKT_DotNetMail(Arg ...

  6. 将bean转换成XML字符串

    package com.sinoservices.bms.bbl.rest.bean; import javax.xml.bind.annotation.XmlAccessType; import j ...

  7. WebDriver中的操作使用

    1.WebDriver中使用jquery假如设定jquery包的路径为path,则程序如下: String jquery=null; FileInputStream input = new FileI ...

  8. Idea如果添加Maven模块

    1.要创建一个和heaton-app同级的Maven模块,如果所示 2.点击下一步,添加ArtifactId,其中 groupId :       定义了项目属于哪个组,举个例子,如果你的公司是myc ...

  9. RequestMethod.Post&RequestMethod.GET

    1.GET和POST都是将数据送到服务器 2.GET通过URL请求传递用户的数据,将表单各字段名称以及内容,以成对的字符串连接,置于action所指程序的URL后:POST方法通过HTTP post ...

  10. spark原理

    SparkContext将应用程序代码分发到各Executors,最后将任务(Task)分配给executors执行 Application: Appliction都是指用户编写的Spark应用程序, ...