解锁 VS Code 更多可能性,轻松入门 WebView

2,655 阅读8分钟

作者:HelloGitHub-小夏

说起 VS Code 大家普遍印象应该都差不多是这样:不就是个编辑器嘛,最主要的还是 coding 的快感咯。

里面很多功能都应该是围绕如何提高 coding 效率、减少 coding 出错率、解放 coder 小哥哥小姐姐的劳动力等等,至于代码以外的东西比如预览什么的,就交给浏览器咯。

所以可能很少有人会把 VS Code 和 WebView 联想到一起。

一、随处可见的 WebView

但是我相信,你一定在很多“有名”的 VS Code 插件中接触过它(WebView)的身影。比如可以在 VS Code 中画流程图的 vscode-drawio:

GitHub 地址:github.com/hediet/vsco…

上班摸鱼的同时还要继续提升自我来刷题的 vscode-leetcode:

GitHub 地址:github.com/LeetCode-Op…

还有上班摸鱼的同时还要关心能否从一颗“小韭菜”实现财富自由的「韭菜盒子」 leek-fund:

GitHub 地址:github.com/LeekHub/lee…

所以你可以看到,有了 WebView 来拓展能力,插件市场才会变得“百花齐放”,能满足各类人各类摸鱼的需求。但是上面开源项目的成功,也不仅仅靠的是我们本文介绍的简单的 WebView 的能力,如果你对上面几个开源项目有深挖的兴趣,可以直接 clone 代码,一瞅到底,说不定下一个厉害的开源 VS Code 插件就是出自你手啦。

二、WebView 到底是什么

前面 有提过 VS Code 允许我们在它给的规则之下可以自定义很多功能,但是视图这一块,其实我们自定义的范围非常小,这就限制了程序员们天马行空的创造力。但是自由的灵魂不会被眼前的困难打败,同行之间的心心相惜所以有了 WebView 的诞生。

当然这都是小编自己内心 OS 的,不过可以确定的是 WebView API 的存在允许在 VS Code 中扩展创建完全可自定义的视图。例如:内置的 Markdown 扩展使用 webviews 来呈现 Markdown 预览。Webviews 还可用于构建超出 VS Code 的本机 API 支持的复杂用户界面。

你也可以简单的把 WebView 理解为 VS Code 内部的 iframe。WebView 可以在这个框架中渲染几乎所有的 HTML 内容,还可以使用消息传递与扩展进行通信。这种自由使得 webviews 非常强大,而且也拥有了一个全新的扩展范围。

三、创建一个简单的 WebView

从第一点的例子你就应该可以体会到 WebView 的功能拓展有多强大,它不仅可以作为自定义编辑器的视图来扩展提供自定义 UI 以编辑工作区中的任何文件。还允许在侧边栏或面板区域的 WebView 中继续呈现 WebView 视图等等。

如果你感兴趣,可以去官网继续学习。今天我们下文谈的主要还是最简单的一种方式:在编辑器中创建一个简单的 WebView 面板。

1、配置命令

第一步首先肯定是配置命令啦,我们再次打开package.json文件,新配置一个command

"contributes": {
		"commands": [
			..., // 省略其他命令
			{
        "command": "webview.start",
        "title": "open a webview page",
        "category": "HelloGitHub webview"
      }
		],
  ... // 省略其他配置项
}

配置完之后要把这个新的命令在 extension.js 中注册一下:

function activate(context) {
  ... // 省略其他命令注册
  
	const webviewCommand = vscode.commands.registerCommand('webview.start', () => {
    // 创建和展示一个 webview
    const panel = vscode.window.createWebviewPanel(
      'hgWebview', // 定义 webview 的类型,用于内部
      'HelloGitHub webview', // 给用户展示的标题
      vscode.ViewColumn.One, // 在第几栏编辑器里展示这个 webview
      {} // 其他 Webview 配置.
    );
  });

	context.subscriptions.push(webviewCommand); // 这里可以放多个,用,分隔即可
}

配置完之后看一眼效果,让我们运行起来我们的插件:

你可以看到这个标题就是我们上面在 package.json 上配置的“HelloGitHub webview”,或许有同学会对 ViewColumn 这个配置疑惑。

那我们来看一下这里到底都有些什么值:

看不懂?没关系,我们实操一下,修改上面在 extension.js 里的配置如下:

const webviewCommand = vscode.commands.registerCommand('webview.start', () => {
  const panel = vscode.window.createWebviewPanel(
    'hgWebview',
    'HelloGitHub webview',
    vscode.ViewColumn.Two, // 从 One 改成 Two
    {}
  );
});

效果如下:

这里多了一个 js 的文件其实没有什么意义,因为如果没有这个文件占编辑器的第一个 ViewColumn 的话,其实效果和上面的配置是一样的,有了这个文件之后,我们的 WebView 才会在第二栏打开。这些单词是不是非常简单易懂?

2、初始化内容

现在我们就要切入最重要的部分啦,如何丰富 WebView 的内容呢?其实也很简单啦,把它看做一个 iframe 就好啦,那无非就是 HTML 的那些东西呗?so easy!

首先我们要有一个包含整个 HTML 内容的独立文件,为了好区分,我把它放在了这里:

配置了一个非常简单的网页内容,里面只有一个图片:

module.exports = `
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello GitHub</title>
</head>
<body>
    <img src="https://cdn.jsdelivr.net/gh/521xueweihan/img_logo@main/logo/readme.gif" width="300" />
</body>
</html>
`

extension.js 中引入文件并配置到我们的 WebView:

const hgWebview = require('./webview/hello-github');

... 
	const webviewCommand = vscode.commands.registerCommand('webview.start', () => {
    const panel = vscode.window.createWebviewPanel(
      'hgWebview',
      'HelloGitHub webview',
      vscode.ViewColumn.One,
      {}
    );
    panel.webview.html = hgWebview; // 对没错就是这里配置,非常简单
  });
...

看一下效果:

这里要提醒大家的是,你配置的应该始终是一个完整的 HTML 文档。HTML 片段或格式错误的 HTML 可能会导致运行不成功,所以在进行复杂操作的时候一定要小心调试,多看控制栏哦。

3、更新内容

是的,我们现在要从编辑器对这个 WebView 做更新操作了!比如我们给这个 WebView 加一行文字,然后在编辑器里面加一个定时器,动态的去修改它。首先,修改我们的 html 文件,它不在是一个静态的文本了,他要动起来就得接收一个变量,所以改成函数咯:

module.exports = (txt) => {
  return `
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Hello GitHub</title>
    </head>
    <body>
        <img src="https://cdn.jsdelivr.net/gh/521xueweihan/img_logo@main/logo/readme.gif" width="300" />
        <div>
          ${txt} // 注意这里是接收变量的写法
        </div>
    </body>
    </html>
  `
}

其次呢,我们要跟这个函数有互动,并将要展示的值传进去,并且这个值还是定时 1s 要进行修改的,所以就变成这样啦:

const hgWebviewFun = require('./webview/hello-github');

// 设置我们的文案
const webviewTxt = {
  'descripton': 'HelloGitHub 是一个热爱开源项目的开源组织。',
  'slogon': '我们虽然没有钱,但是我们有梦想!'
};

...
	const webviewCommand = vscode.commands.registerCommand('webview.start', () => {
		const panel = vscode.window.createWebviewPanel(
			'hgWebview',
			'HelloGitHub webview',
			vscode.ViewColumn.One,
			{}
		);

		let iteration = 0;
		const updateWebview = () => {
      // 做一个简单的判断用于取值
			const key = iteration++ % 2 ? 'descripton' : 'slogon';
			panel.title = webviewTxt[key];
			panel.webview.html = hgWebviewFun(webviewTxt[key]);
		};

		// 设置初始化的内容
		updateWebview();

		// 设置一个简单的定时器,让他一秒内执行一次
		setInterval(updateWebview, 1000);
	});
...

看一下我们的效果,是不是就变成一个动感十足的网页啦:

但是效果是实现了,你有没有发现我们实现的方法非常的“暴力”,是直接替换了整个 html 的内容,类似于重新加载 iframe。所以要是换到复杂的页面,性能肯定是个非常严重的问题,就会导致非常多令人头大的性能问题。而且当用户关闭 WebView 面板时,WebView 本身是会被销毁的。如果尝试使用销毁的 WebView 会引发异常,比如我们上面的 setInterval 会继续触发并更新 panel.webview.html

所以我们要避免这种情况出现:

const webviewCommand = vscode.commands.registerCommand('webview.start', () => {
  const panel = vscode.window.createWebviewPanel(
    'hgWebview',
    'HelloGitHub webview',
    vscode.ViewColumn.One,
    {}
  );

  let iteration = 0;
  const updateWebview = () => {
    const key = iteration++ % 2 ? 'descripton' : 'slogon';
    panel.title = webviewTxt[key];
    panel.webview.html = hgWebviewFun(webviewTxt[key]);
  };

  updateWebview();
  const interval = setInterval(updateWebview, 1000);

  panel.onDidDispose(
    () => {
      // 当关闭 webview 的时候去掉对 webview 有后续更新的操作
      clearInterval(interval);
    },
    null,
    context.subscriptions
  );
});

4、消息传递

前面说过,你可以简单的把 WebView 理解成 iframe,那这也意味着它们都可以运行脚本。不过默认情况下 WebView 中禁用 JavaScript,你可以通过传入 enableScripts: true 来启用。不过官网建议 WebView 应始终使用内容安全策略禁用内联脚本,所以我们这里就不做展开。但是这一点也不影响我们发挥 WebView 的巨大作用——消息传递。

WebView 调试

在消息传递内容之前,我觉得有必要说一下这个调试工具命令 Developer: Toggle Developer Tools。你可以通过 comand+p(MacOS)唤起这个开发者调试命令,可以帮你在调试 WebView 的时候“如鱼得水”,轻松捕获异常并 fix

当然你还可以在 Elements 里面查看 dom 的结构,简直就是太熟悉了~

WebView 接收消息

首先我们先来了解一下如何从我们的插件应用向我们的 webview 传递消息。聪明的你一定猜到了对不对?没错就是 postMessage

修改我们的注册命令如下:

  • createWebviewPanel 的变量存到一个新的变量上去

  • 新增了一个用于消息传递的命令 webview.doRefactor

  • 同时因为在 HTML 内部需要监听 message 的传递,所以我们必须确保开启脚本,也就是上文说的 enableScripts:true

  • 为了确保我们不眼花缭乱,这里也去掉了之前的定时器 setInterval

...	
	let currentPanel; // 重新定义一个变量用于多个命令之间的使用
	const webviewCommand = vscode.commands.registerCommand('webview.start', () => {
		currentPanel = vscode.window.createWebviewPanel(
			'hgWebview',
			'HelloGitHub webview',
			vscode.ViewColumn.One,
			{
				enableScripts: true // 开启 js 脚本权限
			}
		);

		let iteration = 0;
		const updateWebview = () => {
			const key = iteration++ % 2 ? 'descripton' : 'slogon';
			currentPanel.title = webviewTxt[key];
			currentPanel.webview.html = hgWebviewFun(webviewTxt[key]);
		};

		updateWebview();
		// const interval = setInterval(updateWebview, 1000); 去掉定时器

		currentPanel.onDidDispose(
			() => {
				// clearInterval(interval); 去掉定时器
				currentPanel = undefined; // 销毁 webview 的时候释放变量
			},
			null,
			context.subscriptions
		);
	});

 // 注册一个新的命令
	const webviewRefactorCommand = vscode.commands.registerCommand('webview.doRefactor', () => {
		if (!currentPanel) {
			return;
		}

		// 向 webview 发送消息
		// 你可以发送任何 JSON 序列化的数据
		currentPanel.webview.postMessage({ command: 'refactor', msg: '请多关注我们~' });
	})
  
  context.subscriptions.push(webviewCommand, webviewRefactorCommand);
 ...

为了防止有人在跟着敲的时候漏掉这一步,我决定还是再提醒一下~要在 package.json 里面加上新注册的这个命令哦:

... 
      {
        "command": "webview.start",
        "title": "open a webview page",
        "category": "HelloGitHub webview"
      },
			{
        "command": "webview.doRefactor",
        "title": "doRefactor a webview page",
        "category": "HelloGitHub webview"
      }
...

有了消息的发送,当然也需要有消息的接收啦!这才能完成通信嘛~所以我们要修改我们的 HTML 文件,加一个用于接收消息的监听:

module.exports = (txt) => {
  return `
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Hello GitHub</title>
    </head>
    <body>
      <img src="https://cdn.jsdelivr.net/gh/521xueweihan/img_logo@main/logo/readme.gif" width="300" />
      <h1 id="message-show">hello</h1>
      <div>
        ${txt}
      </div>
      <script>
        const box = document.getElementById('message-show');

        // 在这里监听消息的发送
        window.addEventListener('message', event => {

            const message = event.data; // 我们插件发送的数据
            console.log(message) // 打印一下看看是什么样子

            switch (message.command) {
                case 'refactor':
                    box.textContent = message.msg;
                    break;
            }
        });
      </script>
    </body>
    </html>
  `
}

上面的够简单吧,我们来看一下效果,记得打开开发者调试工具,首先是用 webview.start 命令打开 WebView:

运行 webview.doRefactor 之后,我们就把我们的值传到了 WebView 里去啦:

WebView 发送消息

WebView 还可以将消息传递回我们的扩展程序。

这主要是通过使用 WebView 的 postMessage 内特殊的 VS Code API 对象上的函数来完成的。要访问 VS Code API 对象,需要在 WebView 内部调用 acquireVsCodeApi 这个函数每个会话只能调用一次。

而且必须保留此方法返回的 VS Code API 实例,并将其分发给任何其他需要使用它的函数。

我们可以使用 VS Code API 的 postMessage 方法在我们的插件中显示来自 WebView 的消息:

const vscode = acquireVsCodeApi(); // 直接使用

vscode.postMessage({ // 发送消息
  command: 'alert',
  text: '🚀 发送成功~感谢老铁~'
})

我们把这个事件触发绑在了一个新的 button 上,完整的代码如下:

module.exports = (txt) => {
  return `
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Hello GitHub</title>
    </head>
    <body>
      <img src="https://cdn.jsdelivr.net/gh/521xueweihan/img_logo@main/logo/readme.gif" width="300" />
      <h1 id="message-show">hello</h1>
      <div>
        ${txt}
      </div>
      <button id="btn_submit">点我发送🚀!</button>
      <script>
        const box = document.getElementById('message-show');
        const vscode = acquireVsCodeApi();

        window.addEventListener('message', event => {

            const message = event.data;
            console.log(message)

            switch (message.command) {
                case 'refactor':
                    box.textContent = message.msg;
                    break;
            }
        });

        document.getElementById('btn_submit').addEventListener('click', function(){
          vscode.postMessage({
            command: 'alert',
            text: '🚀 发送成功~感谢老铁~'
          })
        })


      </script>
    </body>
    </html>
  `
}

同时也需要在我们的插件代码里接收来自 WebView 的消息:

...
currentPanel.webview.onDidReceiveMessage(
  message => {
    switch (message.command) {
      case 'alert':
        vscode.window.showInformationMessage(message.text);
        return;
    }
  },
  undefined,
  context.subscriptions
);
...

完整的代码如下,在打开 WebView 的时候就要将事件绑定都搞定:

...
 const webviewCommand = vscode.commands.registerCommand('webview.start', () => {
		currentPanel = vscode.window.createWebviewPanel(
			'hgWebview',
			'HelloGitHub webview',
			vscode.ViewColumn.One,
			{
				enableScripts: true
			}
		);

		let iteration = 0;
		const updateWebview = () => {
			const key = iteration++ % 2 ? 'descripton' : 'slogon';
			currentPanel.title = webviewTxt[key];
			currentPanel.webview.html = hgWebviewFun(webviewTxt[key]);
		};

		updateWebview();
		// const interval = setInterval(updateWebview, 1000);

		currentPanel.onDidDispose(
			() => {
				// clearInterval(interval);
				currentPanel = undefined;
			},
			null,
			context.subscriptions
		);

		// 处理来自 webview 的消息
		currentPanel.webview.onDidReceiveMessage(
			message => {
				switch (message.command) {
					case 'alert':
						vscode.window.showInformationMessage(message.text);
						return;
				}
			},
			undefined,
			context.subscriptions
		);
	});
...

接下来我们先看一下点击按钮前的样式:

来看一下我们点击按钮会发生什么“神奇”的事情呢?

四、总结

那快乐的时光总是短暂的,又到了文章结束的时候啦。总的来说 WebView 就像是在 VS Code 里的 iframe,虽然可能在性能上有那么点弊端,但是却能够帮助我们实现很多丰富而又有趣的事情。

因此我们更要好好的利用这个功能,把它的力量发挥到极致。根据官网的描述,我们也要在使用的时候多注意以下几点:

  • WebView 应该具有它所需的最少功能集。例如:如果不需要运行脚本,则不要设置 enableScripts: true

  • WebView 严格遵从 内容安全策略,所以在 WebView 中可加载和执行的内容都有一定的限制。例如:内容安全策略可以确保仅允许在 WebView 中运行的脚本列表,甚至告诉 WebView 只能加载 https 图像。

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

  • 就像普通网页都要求的那样,在为 WebView 构建 HTML 时,必须清理所有用户输入。未能正确清理输入可能会导致内容注入,这可能会使你的用户面临安全风险。比如:文件内容、文件和文件夹路径、用户和工作区设置

  • WebView 有自己的生命周期,如果在有极致体验的场景下发挥他的最大作用,建议去官网更加深入的学习一下

最后的最后,预告一下下一篇「VS Code」系列文章,也就是本入门系列最后一篇文章将会带大家体验更综合性的东西,给小编多一点点时间努力研究一下,期待我们下次再见咯!