VS Code Webview API

webview API为开发者提供了完全自定义视图的能力,例如内置的Markdown插件使用了webview渲染Markdown预览文件。Webview也能用于构建比VS Code原生API支持构建的更加复杂的用户交互界面。

可以把webview看成是VS Code中的iframe,它可以渲染几乎全部的HTML内容,它通过消息机制和插件通信。这样的自由度令我们的webview非常强劲并将插件的潜力提升到了新的高度。

我应该用webview吗?


webview虽然很赞,但是我们应该节制地使用这个功能——比如当VS Code原生API不够用时。Webview重度依赖资源,所以它脱离插件的进程而单独运行在其他环境中。在VS Code中使用设计不良的webview会让用户抓狂。

在使用webview之前,请作以下考虑:

  • 这个功能真的需要VS Code来提供吗?分离成一个应用或者网站会不会更好?
  • webview是实现这个特性的最后方案吗?VS Code原生API是否能达到同样的目的呢?
  • 你的webview所牺牲的高资源占用是否能换得同样的用户价值?

请记住:不要因为能使用webview而滥用webview。相反,如果你有充足的理由和自信,那么本篇教程对你来说会非常有用,现在就让我们开始吧。

Webviews API 基础


为了解释webviewAPI,我们先构建一个简单的Cat Coding插件。这个插件会用一个webview显示猫写代码的gif。随着我们不断了解API,我们会不断地给插件添加功能,包括我们的猫写了多少行代码的计数跟踪器,如果猫猫写出了bug还会有一个提示弹出框。

这是Cat Coding插件的第一版package.json,你可以在这里找到完整的代码。我们的第一版插件提供了一个命令,叫做catCoding.start。当用户从命令面板调用这个Cat Coding: Start new cat coding session

  1. {
  2. "name": "cat-coding",
  3. "description": "Cat Coding",
  4. "version": "0.0.1",
  5. "publisher": "bierner",
  6. "engines": {
  7. "vscode": "^1.23.0"
  8. },
  9. "activationEvents": [
  10. "onCommand:catCoding.start"
  11. ],
  12. "main": "./out/src/extension",
  13. "contributes": {
  14. "commands": [
  15. {
  16. "command": "catCoding.start",
  17. "title": "Start new cat coding session",
  18. "category": "Cat Coding"
  19. }
  20. ]
  21. },
  22. "scripts": {
  23. "vscode:prepublish": "tsc -p ./",
  24. "compile": "tsc -watch -p ./",
  25. "postinstall": "node ./node_modules/vscode/bin/install"
  26. },
  27. "dependencies": {
  28. "vscode": "*"
  29. },
  30. "devDependencies": {
  31. "@types/node": "^9.4.6",
  32. "typescript": "^2.8.3"
  33. }
  34. }

现在让我们实现catCoding.start命令,在我们的主文件中,像下面这样注册一个基础的webview:

  1. import * as vscode from 'vscode';
  2. export function activate(context: vscode.ExtensionContext) {
  3. context.subscriptions.push(vscode.commands.registerCommand('catCoding.start', () => {
  4. // 创建并显示新的webview
  5. const panel = vscode.window.createWebviewPanel(
  6. 'catCoding', // 只供内部使用,这个webview的标识
  7. "Cat Coding", // 给用户显示的面板标题
  8. vscode.ViewColumn.One, // 给新的webview面板一个编辑器视图
  9. { } // Webview选项。我们稍后会用上。
  10. );
  11. }));
  12. }

vscode.window.createWebviewPanel函数创建并在编辑区展示了一个webview,下图显示了如果你试着运行catCoding.start命令会显示的东西:

cat coding

我们的命令以正确的标题打开了一个新的webview面板,但是没有任何内容!要想把我们的猫加到这个面板里面,我们需要webview.html设置HTML内容。

  1. import * as vscode from 'vscode';
  2. export function activate(context: vscode.ExtensionContext) {
  3. context.subscriptions.push(vscode.commands.registerCommand('catCoding.start', () => {
  4. // 创建和显示webview
  5. const panel = vscode.window.createWebviewPanel('catCoding', "Cat Coding", vscode.ViewColumn.One, { });
  6. // 设置HTML内容
  7. panel.webview.html = getWebviewContent();
  8. }));
  9. }
  10. function getWebviewContent() {
  11. return `<!DOCTYPE html>
  12. <html lang="en">
  13. <head>
  14. <meta charset="UTF-8">
  15. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  16. <title>Cat Coding</title>
  17. </head>
  18. <body>
  19. <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
  20. </body>
  21. </html>`;
  22. }

如果你再次运行命令,应该能看到下图:

cat coding

大功告成!

webview.html应该是一个完整的HTML文档。使用HTML片段或者格式错乱的HTML会造成异常。

更新webview内容

webview.html也能在webview创建后更新内容,让我们用猫猫轮播图使Cat Coding具有动态性:

  1. import * as vscode from 'vscode';
  2. const cats = {
  3. 'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
  4. 'Compiling Cat':'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif'
  5. };
  6. export function activate(context: vscode.ExtensionContext) {
  7. context.subscriptions.push(vscode.commands.registerCommand('catCoding.start', () => {
  8. const panel = vscode.window.createWebviewPanel('catCoding', "Cat Coding", vscode.ViewColumn.One, { });
  9. let iteration = 0;
  10. const updateWebview = () => {
  11. const cat = iteration++ % 2 ? 'Compiling Cat' : 'Coding Cat'
  12. panel.title = cat;
  13. panel.webview.html = getWebviewContent(cat);
  14. }
  15. // 设置初始化内容
  16. updateWebview();
  17. // 每秒更新内容
  18. setInterval(updateWebview, 1000);
  19. }));
  20. }
  21. function getWebviewContent(cat: keyof typeof cats) {
  22. return `<!DOCTYPE html>
  23. <html lang="en">
  24. <head>
  25. <meta charset="UTF-8">
  26. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  27. <title>Cat Coding</title>
  28. </head>
  29. <body>
  30. <img src="${cats[cat]}" width="300" />
  31. </body>
  32. </html>`;
  33. }

Webview API - 图3

因为webview.html方法替换了整个webview内容,页面看起来像重新加载了一个iframe。记住:如果你在webview中使用了脚本,那就意味着webview.html的重置会使脚本状态重置。

上述示例也使用了webview.title改变编辑器中的展示的文件名称,设置标题不会使webview重载。

生命周期

webview从属于创建他们的插件,插件必须保持住从webview返回的createWebviewPanel。如果你的插件失去了这个关联,它就不能再访问webview了,不过即使这样,webview还会继续展示在VS Code中。

因为webview是一个文本编辑器视图,所以用户可以随时关闭webview。当用户关闭了webview面板后,webview就被销毁了。在我们的例子中,销毁webview时抛出了一个异常,说明我们上面的示例中使用的seInterval实际上产生了非常严重的Bug:如果用户关闭了面板,setInterval会继续触发,而且还会尝试更新panel.webview.html,这当然会抛出异常。喵星人可不喜欢异常,我们现在就来解决这个问题吧。

onDidDispose事件在webview被销毁时触发,我们在这个事件结束之后更新并释放webview资源。

  1. import * as vscode from 'vscode';
  2. const cats = {
  3. 'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
  4. 'Compiling Cat':'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif'
  5. };
  6. export function activate(context: vscode.ExtensionContext) {
  7. context.subscriptions.push(vscode.commands.registerCommand('catCoding.start', () => {
  8. const panel = vscode.window.createWebviewPanel('catCoding', "Cat Coding", vscode.ViewColumn.One, {});
  9. let iteration = 0;
  10. const updateWebview = () => {
  11. const cat = iteration++ % 2 ? 'Compiling Cat' : 'Coding Cat'
  12. panel.title = cat;
  13. panel.webview.html = getWebviewContent(cat);
  14. }
  15. updateWebview();
  16. const interval = setInterval(updateWebview, 1000);
  17. panel.onDidDispose(() => {
  18. // 当面板关闭时,取消webview内容之后的更新
  19. clearInterval(interval);
  20. }, null, context.subscriptions)
  21. }));
  22. }

插件也可以通过编程方式关闭webview视图——调用它们的dispose()方法。我们假设,现在限制我们的猫猫每天工作5秒钟:

  1. export function activate(context: vscode.ExtensionContext) {
  2. context.subscriptions.push(vscode.commands.registerCommand('catCoding.start', () => {
  3. const panel = vscode.window.createWebviewPanel('catCoding', "Cat Coding", vscode.ViewColumn.One, {});
  4. panel.webview.html = getWebviewContent('https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif');
  5. // 5秒后,程序性地关闭webview面板
  6. const timeout = setTimeout(() => panel.dispose(), 5000)
  7. panel.onDidDispose(() => {
  8. // 在第五秒结束之前处理用户手动的关闭动作
  9. clearTimeout(timeout);
  10. }, null, context.subscriptions)
  11. }));
  12. }

移动和可见性

当webview面板被移动到了非激活标签上,它就隐藏起来了。但这时并不是销毁,当重新激活标签后,VS Code会从webview.html自动恢复webview的内容。

restore

.visible属性告诉你当前webview面板是否是可见的。

插件也可以通过调用reveal()方法,程序性地将webview面板激活。这个方法可以接受一个用于放置面板的目标视图布局。一个面板一次只能显示在一个编辑布局中。调用reveal()或者拖动webview面板到新的编辑布局中去。

Webview API - 图5

现在更新我们的插件,一次只允许存在一个webview视图。如果面板处于非激活状态,那catCoding.start命令就把这个面板激活。

  1. export function activate(context: vscode.ExtensionContext) {
  2. // 追踪当前webview面板
  3. let currentPanel: vscode.WebviewPanel | undefined = undefined;
  4. context.subscriptions.push(vscode.commands.registerCommand('catCoding.start', () => {
  5. const columnToShowIn = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined;
  6. if (currentPanel) {
  7. // 如果我们已经有了一个面板,那就把它显示到目标列布局中
  8. currentPanel.reveal(columnToShowIn);
  9. } else {
  10. // 不然,创建一个新面板
  11. currentPanel = vscode.window.createWebviewPanel('catCoding', "Cat Coding", columnToShowIn, {});
  12. currentPanel.webview.html = getWebviewContent('https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif');
  13. // 当前面板被关闭后重置
  14. currentPanel.onDidDispose(() => {
  15. currentPanel = undefined;
  16. }, null, context.subscriptions);
  17. }
  18. }));
  19. }

Webview API - 图6

不论何时,如果webview的可见性改变了,或者当webview移动到了新的视图布局中,就会触发onDidChangeViewState。我们的插件可以利用这个时间改变布局中的webview显示的猫:

  1. const cats = {
  2. 'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
  3. 'Compiling Cat':'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif',
  4. 'Testing Cat': 'https://media.giphy.com/media/3oriO0OEd9QIDdllqo/giphy.gif'
  5. };
  6. export function activate(context: vscode.ExtensionContext) {
  7. context.subscriptions.push(vscode.commands.registerCommand('catCoding.start', () => {
  8. const panel = vscode.window.createWebviewPanel('catCoding', "Cat Coding", vscode.ViewColumn.One, {});
  9. panel.webview.html = getWebviewContent(cats['Coding Cat']);
  10. // 根据视图状态变动更新内容
  11. panel.onDidChangeViewState(e => {
  12. const panel = e.webviewPanel;
  13. switch (panel.viewColumn) {
  14. case vscode.ViewColumn.One:
  15. updateWebviewForCat(panel, 'Coding Cat');
  16. return;
  17. case vscode.ViewColumn.Two:
  18. updateWebviewForCat(panel, 'Compiling Cat');
  19. return;
  20. case vscode.ViewColumn.Three:
  21. updateWebviewForCat(panel, 'Testing Cat');
  22. return;
  23. }
  24. }, null, context.subscriptions);
  25. }));
  26. }
  27. function updateWebviewForCat(panel: vscode.WebviewPanel, catName: keyof typeof cats) {
  28. panel.title = catName;
  29. panel.webview.html = getWebviewContent(cats[catName]);
  30. }

ondidchangeview state

检查和调试webviews

在命令面板中输入Developer: Toggle Developer Tools能帮助你调试webview。运行命令之后会为当前可见的webview加载一个devtool:

webview Developer Tools

webview的内容是在webview文档中的一个iframe中的,用开发者工具检查和修改webview的DOM,在webview内调试脚本。

如果你用了webview开发者工具的console,确保你在Console面板左上角的下拉框里选中了当前激活窗体环境: debug-active-frame

激活窗体环境是webview脚本执行的地方,另外,Developer: Reload Webview命令会刷新所有已激活的webview。如果你需要重置一个webview的状态,这个命令会非常有用,或者你想要读取硬盘内容的webview更新一下,也可以使用这个方法。

加载本地内容


webview运行在独立的环境中,因此不能直接访问本地资源,这是出于安全性考虑的做法。这也意味着要想从你的插件中加载图片、样式等其他资源,或是从用户当前的工作区加载任何内容的话,你必须使用webview中的vscode-resource:协议。

vscode-resource:协议就像file:协议一样,不过它只允许访问本地文件。和file:一样的是,vscode-resource:只能从绝对路径中加载资源。

想想一下,我们想要从本地把喵喵们的gif打包进来,而不是从Giphy(国外出名的gif收集站)里加载进来。要想做到这点,我们首先给本地文件新建一个URI,然后用vscode-resource:协议更新这些URI:

  1. import * as vscode from 'vscode';
  2. import * as path from 'path';
  3. export function activate(context: vscode.ExtensionContext) {
  4. context.subscriptions.push(vscode.commands.registerCommand('catCoding.start', () => {
  5. const panel = vscode.window.createWebviewPanel('catCoding', "Cat Coding", vscode.ViewColumn.One, { });
  6. // 获取磁盘上的资源路径
  7. const onDiskPath = vscode.Uri.file(path.join(extensionPath, 'media', 'cat.gif'));
  8. // 获取在webview中使用的特殊URI
  9. const catGifSrc = onDiskPath.with({ scheme: 'vscode-resource' });
  10. panel.webview.html = getWebviewContent(catGifSrc);
  11. }));
  12. }

catGifSrc的值最后会像这样

  1. vscode-resource:/Users/toonces/projects/vscode-cat-coding/media/cat.gif

默认情况下,scode-resource:只能访问下列地址的资源:

  • 你的插件安装的目录
  • 用户当前激活的工作区

你也可以用data URI将资源直接嵌套到webview中去。

控制本地资源访问

使用localResourceRoots选项,webview可以控制vscode-resource:加载的的资源。 localResourceRoots定义了可能被加载的本地内容的根URI。

我们用localResourceRoots去约束Cat Codingwebview只加载我们插件的media目录下的内容:

  1. import * as vscode from 'vscode';
  2. import * as path from 'path';
  3. export function activate(context: vscode.ExtensionContext) {
  4. context.subscriptions.push(vscode.commands.registerCommand('catCoding.start', () => {
  5. const panel = vscode.window.createWebviewPanel('catCoding', "Cat Coding", vscode.ViewColumn.One, {
  6. // Only allow the webview to access resources in our extension's media directory
  7. localResourceRoots: [
  8. vscode.Uri.file(path.join(extensionPath, 'media'))
  9. ]
  10. });
  11. const onDiskPath = vscode.Uri.file(path.join(extensionPath, 'media', 'cat.gif'));
  12. const catGifSrc = onDiskPath.with({ scheme: 'vscode-resource' });
  13. panel.webview.html = getWebviewContent(catGifSrc);
  14. }));
  15. }

为了禁止所有的本地资源,只要把localResourceRoots设为[]就好了。 通常来说,webview应该和加载本地资源一样严格,然而,vscode-resourcelocalResourceRoots并不保证百分百的安全性。请确保你的webview遵循安全性最佳实践,强烈建议考虑添加一个内容安全政策以便约束之后加载的内容。

给webview内容加上主题

webview可以基于当前的VS Code主题和CSS改变自身的样式。VS Code将主题分成3中类别,而且在body元素上加上了特殊类名以表明当前主题:

  • vscode-light——亮色主题
  • vscode-dark——暗色主题
  • vscode-high-contrast——高反差主题

下列CSS改变了基于用户当前主题的webview字体颜色:

  1. body.vscode-light {
  2. color: black;
  3. }
  4. body.vscode-dark {
  5. color: white;
  6. }
  7. body.vscode-high-contrast {
  8. color: red;
  9. }

当开发一个webview应用的时候,请保证应用能在三种主题下都可以运作,务必在高反差模式下测试你的webview,以便有视觉障碍的用户也能正常使用。

webview可以通过CSS variables访问VS Code主题,这些变量以vscode为前缀,并且用-替代了.,例如editor.foreground变成了var(--vscode-editor-foreground)

  1. code {
  2. color: var(--vscode-editor-foreground);
  3. }

脚本和信息传递


既然webview就像iframe一样,也就是说它们也可以运行脚本,webview中的Javascript默认是禁用的,不过我们能用enableScripts: true打开它。

让我们写一段脚本,追踪我们家喵星人写代码的行数。运行一个基础脚本非常的容易,但是注意这个示例只作演示用途,在实践中,你的webview应该遵循内容安全政策,禁止行内脚本。

  1. import * as path from 'path';
  2. import * as vscode from 'vscode';
  3. export function activate(context: vscode.ExtensionContext) {
  4. context.subscriptions.push(vscode.commands.registerCommand('catCoding.start', () => {
  5. const panel = vscode.window.createWebviewPanel('catCoding', "Cat Coding", vscode.ViewColumn.One, {
  6. // 在webview中启用脚本
  7. enableScripts: true
  8. });
  9. panel.webview.html = getWebviewContent();
  10. }));
  11. }
  12. function getWebviewContent() {
  13. return `<!DOCTYPE html>
  14. <html lang="en">
  15. <head>
  16. <meta charset="UTF-8">
  17. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  18. <title>Cat Coding</title>
  19. </head>
  20. <body>
  21. <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
  22. <h1 id="lines-of-code-counter">0</h1>
  23. <script>
  24. const counter = document.getElementById('lines-of-code-counter');
  25. let count = 0;
  26. setInterval(() => {
  27. counter.textContent = count++;
  28. }, 100);
  29. </script>
  30. </body>
  31. </html>`;
  32. }

scripts-basic

哇!真是位高产的喵主子!

!> webveiw的脚本能做到任何普通网页脚本能做到的事情,但是webview运行在自己的上下文中,脚本不能访问VS Code API。

将插件的信息传递到webview

插件可以用webview.postMessage()将数据发送到它的webview中。这个方法能发送任何序列化的JSON数据到webview中,在webview中则通过message事件接受信息。

我们现在就来看看这个实现,在Cat Coding中新增一个命令来表示我们家的喵在重构他的代码(所以会减少代码总行数)。新增catCoding.doRefactor命令,利用postMessage把指示发送到webview中,webview中的window.addEventListener('message' event => { ... })则会处理这些信息:

  1. export function activate(context: vscode.ExtensionContext) {
  2. // 现在只有一只喵喵程序员了
  3. let currentPanel: vscode.WebviewPanel | undefined = undefined;
  4. context.subscriptions.push(vscode.commands.registerCommand('catCoding.start', () => {
  5. if (currentPanel) {
  6. currentPanel.reveal(vscode.ViewColumn.One);
  7. } else {
  8. currentPanel = vscode.window.createWebviewPanel('catCoding', "Cat Coding", vscode.ViewColumn.One, {
  9. enableScripts: true
  10. });
  11. currentPanel.webview.html = getWebviewContent();
  12. currentPanel.onDidDispose(() => { currentPanel = undefined; }, undefined, context.subscriptions);
  13. }
  14. }));
  15. // 我们新的命令
  16. context.subscriptions.push(vscode.commands.registerCommand('catCoding.doRefactor', () => {
  17. if (!currentPanel) {
  18. return;
  19. }
  20. // 把信息发送到webview
  21. // 你可以发送任何序列化的JSON数据
  22. currentPanel.webview.postMessage({ command: 'refactor' });
  23. }));
  24. }
  25. function getWebviewContent() {
  26. return `<!DOCTYPE html>
  27. <html lang="en">
  28. <head>
  29. <meta charset="UTF-8">
  30. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  31. <title>Cat Coding</title>
  32. </head>
  33. <body>
  34. <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
  35. <h1 id="lines-of-code-counter">0</h1>
  36. <script>
  37. const counter = document.getElementById('lines-of-code-counter');
  38. let count = 0;
  39. setInterval(() => {
  40. counter.textContent = count++;
  41. }, 100);
  42. // Handle the message inside the webview
  43. window.addEventListener('message', event => {
  44. const message = event.data; // The JSON data our extension sent
  45. switch (message.command) {
  46. case 'refactor':
  47. count = Math.ceil(count * 0.5);
  48. counter.textContent = count;
  49. break;
  50. }
  51. });
  52. </script>
  53. </body>
  54. </html>`;

将webview的信息传递到插件中

webview也可以把信息传递回对应的插件中,用VS Code API 为webview提供的postMessage函数我们就可以完成这个目标。调用webview中的acquireVsCodeApi获取VS Code API对象。这个函数在一个会话中只能调用一次,你必须保持住这个方法返回的VS Code API实例,然后再转交到需要调用这个实例的地方。

现在我们在Cat Coding添加一个弹出bug的警示框:

  1. export function activate(context: vscode.ExtensionContext) {
  2. context.subscriptions.push(vscode.commands.registerCommand('catCoding.start', () => {
  3. const panel = vscode.window.createWebviewPanel('catCoding', "Cat Coding", vscode.ViewColumn.One, {
  4. enableScripts: true
  5. });
  6. panel.webview.html = getWebviewContent();
  7. // 处理webview中的信息
  8. panel.webview.onDidReceiveMessage(message => {
  9. switch (message.command) {
  10. case 'alert':
  11. vscode.window.showErrorMessage(message.text);
  12. return;
  13. }
  14. }, undefined, context.subscriptions);
  15. }));
  16. }
  17. function getWebviewContent() {
  18. return `<!DOCTYPE html>
  19. <html lang="en">
  20. <head>
  21. <meta charset="UTF-8">
  22. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  23. <title>Cat Coding</title>
  24. </head>
  25. <body>
  26. <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
  27. <h1 id="lines-of-code-counter">0</h1>
  28. <script>
  29. (function() {
  30. const vscode = acquireVsCodeApi();
  31. const counter = document.getElementById('lines-of-code-counter');
  32. let count = 0;
  33. setInterval(() => {
  34. counter.textContent = count++;
  35. // Alert the extension when our cat introduces a bug
  36. if (Math.random() < 0.001 * count) {
  37. vscode.postMessage({
  38. command: 'alert',
  39. text: '🐛 on line ' + count
  40. })
  41. }
  42. }, 100);
  43. }())
  44. </script>
  45. </body>
  46. </html>`;
  47. }

scripts-webview_to_extension

出于安全性考虑,你必须保证VS Code API的私有性,也不会泄露到全局状态中去。

安全性


每一个你创建的webview都必须遵循这些基础的安全性最佳实践。

限制能力

webview应该留有它所需的最小功能集合即可。例如:如果你的webview不需要运行脚本,就不要设置enableScripts: true。如果你的webview不需要加载用户工作区的资源,就把localResourceRoots设置为[vscode.Uri.file(extensionContext.extensionPath)]或者[]以便禁止访问任何本地资源。

内容安全策略

内容安全策略可以进一步限制webview可以加载和执行的内容。例如:内容安全策略强制可以运行在webview中的脚本白名单,或者告诉webview只加载带https协议的图片。

要想加上内容安全策略,将<meta http-equiv="Content-Security-Policy">指令放到webview的<head>

  1. function getWebviewContent() {
  2. return `<!DOCTYPE html>
  3. <html lang="en">
  4. <head>
  5. <meta charset="UTF-8">
  6. <meta http-equiv="Content-Security-Policy" content="default-src 'none';">
  7. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  8. <title>Cat Coding</title>
  9. </head>
  10. <body>
  11. ...
  12. </body>
  13. </html>`;
  14. }

default-src 'none';策略直接禁止了所有内容。我们可以按插件需要的最少内容修改这个指令,如只允许通过https加载本地脚本、样式和图片:

  1. <meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src vscode-resource: https:; script-src vscode-resource:; style-src vscode-resource:;">

上述策略也隐式地禁用了内联脚本和样式。把内联样式和脚本提取到外部文件中是一个非常好的实践,也不会与内容安全策略冲突。

只通过https加载内容

如果你的webview允许加载外部资源,我们强烈建议你只允许通过https加载而不要使用http,上面的例子已经用内容安全策略展示了使用https的方式。

审查用户输入

就像构建普通HTML页面一样,你也同样需要在webview中审查用户输入的内容。 没有审查输入内容可能会导致内容注入,也就意味着将用户置于了危险之中。

可能需要审查的值:

  • 文件内容
  • 文件和文件夹路径
  • 用户工作区设置

可以考虑用一个辅助库去构建HTML模板,或者确保所有来自用户工作区的内容都通过了审查

只依赖审查内容的安全性是不够的,你也要遵循其他安全性的最佳实践,尽可能减少潜在的内容注入。

持久性


在webview的标准生命周期中,createWebviewPanel负责创建和销毁(用户关闭或者调用.dispose()方法)webview。而webview的内容再是在webview可见时创建的,在webview处于非激活状态时销毁。webview处于非激活标签中时,任何webview中的保留的状态都会丢失。

所以最好减少webview中的状态,取而代之用消息传递储存状态。

getState和setState

运行在webview中的脚本可以使用getStatesetState方法保存和恢复JSON序列化的状态对象。这个状态可以一直保留,即使webview面板已经被隐藏,只有当它销毁时,状态则会一起销毁。

  1. // webview中的脚本
  2. const vscode = acquireVsCodeApi();
  3. const counter = document.getElementById('lines-of-code-counter');
  4. // 检查是否需要恢复状态
  5. const previousState = vscode.getState();
  6. let count = previousState ? previousState.count : 0;
  7. counter.textContent = count;
  8. setInterval(() => {
  9. counter.textContent = count++;
  10. // 更新已经保存的状态
  11. vscode.setState({ count })
  12. }, 100);

getStatesetState是用来保存状态的比较好的办法,因为他们的性能消耗要远低于retainContextWhenHidden

序列化

使用WebviewPanelSerializer之后,你的webview可以在VS Code关闭后自动恢复。序列化构建于getStatesetState之上,只有你的插件注册了WebviewPanelSerializer,这个功能才会生效。

给插件的package.json添加一个onWebviewPanel激活事件,然后我们的代码喵就能在VS Code重启后继续工作了:

  1. "activationEvents": [
  2. ...,
  3. "onWebviewPanel:catCoding"
  4. ]

这个激活事件确保我们的插件不论VS Code何时恢复catCodingwebview时都会启动。

然后在我们插件的activate方法中调用registerWebviewPanelSerializer注册一个新的WebviewPanelSerializer,这个函数负责恢复webview之前保存的内容。其中的state就是webview用setState设置的JSON格式的状态。

  1. export function activate(context: vscode.ExtensionContext) {
  2. // 常见设置...
  3. // 确保我们注册了一个序列化器
  4. vscode.window.registerWebviewPanelSerializer('catCoding', new CatCodingSerializer());
  5. }
  6. class CatCodingSerializer implements vscode.WebviewPanelSerializer {
  7. async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel, state: any) {
  8. // `state`是webview内调用`setState`保留住的
  9. console.log(`Got state: ${state}`);
  10. // 恢复我们的webview内容
  11. //
  12. // 确保我们将`webviewPanel`传递到了这里
  13. // 然后用事件侦听器恢复我们的内容
  14. webviewPanel.webview.html = getWebviewContent();
  15. }
  16. }

在VS Code中打开一个喵喵打代码的面板,关闭后重启就能看到这个面板恢复到了之前的状态和位置。

隐藏时保留上下文

如果webview的视图非常复杂,或者状态不能很快地保存和恢复,你则可以用retainContextWhenHidden选项,这个选项在不可见的状态中保存了webview的内容,即使webview本身不处于激活状态。

虽然Cat Coding说不上有很复杂的状态,不过我们可以打开retainContextWhenHidden看看webview的行为会发生什么变化:

  1. import * as vscode from 'vscode';
  2. export function activate(context: vscode.ExtensionContext) {
  3. context.subscriptions.push(vscode.commands.registerCommand('catCoding.start', () => {
  4. const panel = vscode.window.createWebviewPanel('catCoding', "Cat Coding", vscode.ViewColumn.One, {
  5. enableScripts: true,
  6. retainContextWhenHidden: true
  7. });
  8. panel.webview.html = getWebviewContent();
  9. }));
  10. }
  11. function getWebviewContent() {
  12. return `<!DOCTYPE html>
  13. <html lang="en">
  14. <head>
  15. <meta charset="UTF-8">
  16. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  17. <title>Cat Coding</title>
  18. </head>
  19. <body>
  20. <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
  21. <h1 id="lines-of-code-counter">0</h1>
  22. <script>
  23. const counter = document.getElementById('lines-of-code-counter');
  24. let count = 0;
  25. setInterval(() => {
  26. counter.textContent = count++;
  27. }, 100);
  28. </script>
  29. </body>
  30. </html>`;
  31. }

persistence-retrain

我们可以注意到计数器没有重置,webview隐藏之后就恢复了。而且不需要多余的代码!retainContextWhenHidden的行为就像浏览器一样,脚本和其他内容被暂时挂起,但是一旦webview可见之后就会立即恢复。但是在webview隐藏状态下,你还是不能给它发送消息的。

虽然retainContextWhenHidden很吸引人,但是记住这个功能的内容占用很高,只有其他的持久化技术无能为力之时再选择这种方式。

下一步

如果你想了解学习更多VS Code扩展性的内容,请查看下列主题: