VSCode插件开发全攻略(七)WebView

摘要:
有关更多文章,请单击VSCode插件开发的完整介绍系列的目录导航。我们都知道Webview是什么。整个VSCode编辑器是一个很大的网页。事实上,我们还可以在Visual Studio代码中创建一个完全定制的特殊网页,它可以与Nodejs间接通信。此网页称为WebView。内置Markdown预览是使用WebView实现的。vscode resource:protocol与file:protocol类似,但它只允许访问特定的本地文件。

更多文章请戳VSCode插件开发全攻略系列目录导航

什么是Webview

大家都知道,整个VSCode编辑器就是一张大的网页,其实,我们还可以在Visual Studio Code中创建完全自定义的、可以间接和nodejs通信的特殊网页(通过一个acquireVsCodeApi特殊方法),这个网页就叫WebView。内置的Markdown的预览就是使用WebView实现的。使用Webview可以构建复杂的、支持本地文件操作的用户界面。

VSCode插件的WebView类似于iframe的实现,但并不是真正的iframe(我猜底层应该还是基于iframe实现的,只不过上层包装了一层),通过开发者工具可以看到:

W1506xH802

demo

在我们的vscode-plugin-demo中,我写了一个非常简单、没啥实际意义的Webview示例仅供参考,在任意编辑器右键可以看到打开Webview的菜单:

W1424xH842

什么时候适合使用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="http://t.zoukankan.com/|<script.+?src="http://t.zoukankan.com/|<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:

W624xH430

插件和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;
}

深色主题效果:

W1404xH770

生命周期

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

W906xH526

审查Webview:

W1152xH1086

这个时候需要特别注意错误日志出现的位置,如果是Webview的错误,一般打印在前面说的这个开发者工具,但如果是插件端的错误只会打印在整个VSCode的开发者工具里。

糟糕,距离最开始接触Webview已经有一段时间了,本来有挺多想写的,但是现在居然没灵感了,额……坑爹啊

W240xH227

参考链接

https://code.visualstudio.com/docs/extensions/webview

免责声明:文章转载自《VSCode插件开发全攻略(七)WebView》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇samba文件共享及账户映射npm run dev时 一堆报错 (node:37693) Warning: Accessing nonexistent property 'chmod' of module exports inside circular dependency下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

JavaScript跨域问题的解决方案

  JS跨域问题在系统与系统的交互过程中会经常出现,比如一个系统的某个页面引用了另外一个系统中页面的内容,并通过引用页面的功能来刷新父页面或者另外的一个页面。这些交互就会出现跨域问题了。   下面我画了一个简单的图来说明: 图文描述: page1的来源是一个A系统,page2是和iframe.aspx是同一个系统B; iframe.aspx中包含...

小程序的当下和未来可能-----------引用

一、小程序历史 HTML5 于 2007 年在 W3C 立项,与 iPhone 发布同年。乔布斯曾期待 HTML5 能帮助 iPhone 打造起应用生态系统。但 HTML5 的发展速度并不如预期,虽然它成功地打破了 IE+Flash 垄断的局面,却没有达到承载优秀的移动互联网体验的地步。苹果公司在 iPhone 站稳脚跟后,紧接着发布了自己的 App S...

webstorm 和 vscode 的纠结

  自从知道了 vscode 之后,尝试了一下后,就开始纠结是否要更换到vscode。 特别是在 webstorm 卡的时候。   也研究了几日 vscode,安装了许多插件。 我是安装插件后, 就开始学习快捷键。   虽然纠结, 不过转换也是非常快。一般查看下自己用的几个快捷键。然后看看是否支持 snippet 功能, 在webstorm 中是 Live...

kivy 使用webview加载网页

from kivy.app import App from kivy.uix.widget import Widget from kivy.clock import Clock from jnius import autoclass from android.runnable import run_on_ui_thread WebView...

设置User Agent

  公司的前端要给项目的webview加一个区分,用来区别是iOS端访问、android访问还是在浏览器访问的,说是要加一个User Agent ,前端根据不同信息做适配,和我说来一头雾水,后来经过开发同事的指导和在网上查阅资料,才有了点头绪,在这里和大家分享一下。 一、获取UserAgent UIWebView方式: UIWebView* tempWeb...

vscode + vim 全键盘操作高效搭配方案

基础知识 vscode-vim vscode-vim是一款vim模拟器,它将vim的大部分功能都集成在了vscode中,你可以将它理解为一个嵌套在vscode中的vim。 由于该vim是被模拟的的非真实vim,所以原生vim中有些功能它并不支持,如宏录制功能,但这依然不妨碍vscode-vim插件的优秀。 其实在vscode的扩展商店中,还有一个vsco...