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

文章转载于:https://www.cnblogs.com/liuxianan/p/vscode-plugin-webview.htmlcss

什么是Webview

你们都知道,整个VSCode编辑器就是一张大的网页,其实,咱们还能够在Visual Studio Code中建立彻底自定义的、能够间接和nodejs通讯的特殊网页(经过一个acquireVsCodeApi特殊方法),这个网页就叫WebView。内置的Markdown的预览就是使用WebView实现的。使用Webview能够构建复杂的、支持本地文件操做的用户界面。html

VSCode插件的WebView相似于iframe的实现,但并非真正的iframe(我猜底层应该仍是基于iframe实现的,只不过上层包装了一层),经过开发者工具能够看到:vue

W1506xH802

1.1. demo

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

W1424xH842

何时适合使用WebView

虽然Webview使人很振奋,由于基于它咱们能够随意发挥不受限制,但必须注意仍是要慎用,毕竟VSCode是很注重性能的,不能由于你一个插件拖累了整个IDE,通常仅在原有API和功能以及交互方式没法知足你时才须要考虑,另外,设计糟糕的Webview也很容易在VS Code中让人感受不温馨,不能让人家一看就以为你这是一张网页,好看的UI也很重要。git

这是官网给出的建议,在使用webview以前请考虑如下事项:github

  • 这个功能真的须要放在VSCode中吗?做为单独的应用程序或网站会不会更好呢?
  • webview是实现这个功能的惟一方法吗?可使用常规VS Code API吗?
  • 您的webview是否会带来足够的用户价值以证实其高资源成本?

正式开始WebView之旅

3.1. 建立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

  • 默认状况下,在Web视图中禁用JavaScript,但能够经过传入enableScripts: true选项轻松启用;
  • 默认状况下当webview被隐藏时资源会被销毁,经过retainContextWhenHidden: true会一直保存,但会占用较大内存开销,仅在须要时开启;

3.2. 加载本地资源

出于安全考虑,Webview默认没法直接访问本地资源,它在一个孤立的上下文中运行,想要加载本地图片、js、css等必须经过特殊的vscode-resource:协议,网页里面全部的静态资源都要转换成这种格式,不然没法被正常加载json

vscode-resource:协议相似于file:协议,但它只容许访问特定的本地文件。和file:同样,vscode-resource:从磁盘加载绝对路径的资源。api

我简单封装了一个转换方法:安全

/**
 * 获取某个扩展文件相对于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中嵌入资源,这种方式没有限制;

3.3. 从文件加载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');

 

3.4. 消息通讯

重头戏来了,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);

 

3.4.1. 简单通讯封装

为了双方通讯方便,我把它们简单封装了一下,仅供参考,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, '打开文件成功!');
    }
};

 

以上封装的比较随便,只是给你们提供一个思路,有时间能够好好封装一下。

3.5. 主题适配

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

3.6. 生命周期

webview由建立它的扩展程序全部,返回的panel对象你必须本身保存,若是你的扩展程序丢失了这个引用,那么将没法再次从新访问该webview,即便Web视图继续显示在vscode中。

用户也能够随时关闭webview面板。当用户关闭webview面板时,webview自己将被销毁,此时不能再使用panel引用,不然将会出现异常,能够经过监听onDidDispose事件在这里面作一些销毁操做。

能够经过panel.dispose()方法主动关闭webview。

3.7. 状态保持

当webview移动到后台又再次显示时,webview中的任何状态都将丢失。

解决此问题的最佳方法是使你的webview无状态,经过消息传递来保存webview的状态。

3.7.1. state

在webview的js中咱们可使用vscode.getState()vscode.setState()方法来保存和恢复JSON可序列化状态对象。当webview被隐藏时,即便webview内容自己被破坏,这些状态仍然会保存。固然了,当webview被销毁时,状态将被销毁。

3.7.2. 序列化

经过注册WebviewPanelSerializer能够实如今VScode重启后自动恢复你的webview,固然,序列化其实也是创建在getStatesetState之上的。

注册方法:vscode.window.registerWebviewPanelSerializer

3.7.3. retainContextWhenHidden

对于具备很是复杂的UI或状态且没法快速保存和恢复的webview,咱们能够直接使用retainContextWhenHidden选项。设置retainContextWhenHidden: true后即便webview被隐藏到后台其状态也不会丢失。

尽管retainContextWhenHidden颇有吸引力,但它须要很高的内存开销,通常建议在实在没办法的时候才启用。
getStatesetState是持久化的首选方式,由于它们的性能开销要比retainContextWhenHidden低得多。

调试

若是配置了快捷键,那么能够直接在插件页面按command+shift+p,便可调出控制台,以下图所示

 

 

注意,要调试Webview不能直接把VSCode的开发者工具打开,直接打开就会和咱们最前面的截图看到的那样,你只能看到一个<webview></webview>标签,看不到代码,要看代码须要按下Ctrl+Shift+P而后执行打开Webview开发工具,英文版应该是Open Webview Developer Tools

W906xH526

审查Webview:

W1152xH1086

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

相关文章
相关标签/搜索