Webview API

webview API 允许扩展在 Visual Studio Code 中创建完全可自定义的视图。例如,内置的 Markdown 扩展使用 webviews 来渲染 Markdown 预览。Webviews 还可以用于构建超出 VS Code 原生 API 支持的复杂用户界面。

将webview视为VS Code中由您的扩展控制的iframe。webview可以在此框架中呈现几乎任何HTML内容,并通过消息传递与扩展进行通信。这种自由使webview变得非常强大,并为扩展开辟了全新的可能性。

Webviews 在多个 VS Code API 中使用:

  • 使用createWebviewPanel创建的Webview面板。在这种情况下,Webview面板在VS Code中显示为独立的编辑器。这使得它们非常适合显示自定义UI和自定义可视化。
  • 作为自定义编辑器的视图。自定义编辑器允许扩展为工作区中的任何文件提供自定义的编辑界面。自定义编辑器API还允许您的扩展挂接到编辑器事件(如撤销和重做)以及文件事件(如保存)。
  • 在侧边栏或面板区域中呈现的Webview视图中。有关更多详细信息,请参阅webview视图示例扩展

本页重点介绍基本的webview面板API,尽管这里涵盖的几乎所有内容也适用于自定义编辑器和webview视图中使用的webview。即使您对那些API更感兴趣,我们也建议您先阅读本页,以熟悉webview的基础知识。

VS Code API 使用

我应该使用webview吗?

Webviews 非常强大,但也应该谨慎使用,只有在 VS Code 的原生 API 不足时才使用。Webviews 资源消耗较大,并且在正常扩展的独立上下文中运行。设计不佳的 webview 也很容易在 VS Code 中显得格格不入。

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

  • 这个功能真的需要在VS Code中实现吗?作为一个独立的应用程序或网站会不会更好?

  • 实现你的功能是否只能通过webview?你能使用常规的VS Code API代替吗?

  • 您的网页视图是否能够增加足够的用户价值,以证明其高资源成本的合理性?

记住:仅仅因为你可以用webviews做某事,并不意味着你应该这样做。然而,如果你确信需要使用webviews,那么这份文档将为你提供帮助。让我们开始吧。

Webviews API 基础

为了解释webview API,我们将构建一个名为Cat Coding的简单扩展。这个扩展将使用webview来展示一只猫在写一些代码的gif(大概是在VS Code中)。随着我们逐步了解API,我们将继续为扩展添加功能,包括一个计数器,用于跟踪我们的猫写了多少行源代码,以及当猫引入错误时通知用户的通知。

这是Cat Coding扩展的第一个版本的package.json。你可以找到示例应用的完整代码这里。我们扩展的第一个版本贡献了一个命令,称为catCoding.start。当用户调用此命令时,我们将显示一个包含我们猫的简单网页视图。用户可以从命令面板中调用此命令,如Cat Coding: 开始新的猫编码会话,如果他们愿意,甚至可以为其创建键绑定。

{
  "name": "cat-coding",
  "description": "Cat Coding",
  "version": "0.0.1",
  "publisher": "bierner",
  "engines": {
    "vscode": "^1.74.0"
  },
  "activationEvents": [],
  "main": "./out/extension.js",
  "contributes": {
    "commands": [
      {
        "command": "catCoding.start",
        "title": "Start new cat coding session",
        "category": "Cat Coding"
      }
    ]
  },
  "scripts": {
    "vscode:prepublish": "tsc -p ./",
    "compile": "tsc -watch -p ./",
    "postinstall": "node ./node_modules/vscode/bin/install"
  },
  "dependencies": {
    "vscode": "*"
  },
  "devDependencies": {
    "@types/node": "^9.4.6",
    "typescript": "^2.8.3"
  }
}

注意: 如果你的扩展目标是一个早于1.74的VS Code版本,你必须在activationEvents中明确列出onCommand:catCoding.start

现在让我们实现catCoding.start命令。在我们的扩展主文件中,我们注册catCoding.start命令并使用它来显示一个基本的webview:

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      // Create and show a new webview
      const panel = vscode.window.createWebviewPanel(
        'catCoding', // Identifies the type of the webview. Used internally
        'Cat Coding', // Title of the panel displayed to the user
        vscode.ViewColumn.One, // Editor column to show the new webview panel in.
        {} // Webview options. More on these later.
      );
    })
  );
}

vscode.window.createWebviewPanel 函数在编辑器中创建并显示一个webview。以下是如果你尝试在当前状态下运行catCoding.start命令时看到的内容:

一个空的webview

我们的命令打开了一个具有正确标题但没有内容的新webview面板!为了将我们的猫添加到新面板中,我们还需要使用webview.html设置webview的HTML内容:

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      // Create and show panel
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {}
      );

      // And set its HTML content
      panel.webview.html = getWebviewContent();
    })
  );
}

function getWebviewContent() {
  return `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cat Coding</title>
</head>
<body>
    <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
</body>
</html>`;
}

如果你再次运行命令,现在webview看起来像这样:

一个包含HTML的webview

进度!

webview.html 应该始终是一个完整的HTML文档。HTML片段或格式错误的HTML可能会导致意外行为。

更新webview内容

webview.html 也可以在创建后更新webview的内容。让我们使用这个功能,通过引入猫咪的轮换来使Cat Coding更加动态:

import * as vscode from 'vscode';

const cats = {
  'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
  'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif'
};

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {}
      );

      let iteration = 0;
      const updateWebview = () => {
        const cat = iteration++ % 2 ? 'Compiling Cat' : 'Coding Cat';
        panel.title = cat;
        panel.webview.html = getWebviewContent(cat);
      };

      // Set initial content
      updateWebview();

      // And schedule updates to the content every second
      setInterval(updateWebview, 1000);
    })
  );
}

function getWebviewContent(cat: keyof typeof cats) {
  return `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cat Coding</title>
</head>
<body>
    <img src="${cats[cat]}" width="300" />
</body>
</html>`;
}

更新webview内容

设置 webview.html 会替换整个 webview 内容,类似于重新加载 iframe。这一点在你开始在 webview 中使用脚本时非常重要,因为这意味着设置 webview.html 也会重置脚本的状态。

上面的例子还使用了 webview.title 来更改编辑器中显示的文档标题。设置标题不会导致 webview 重新加载。

生命周期

Webview 面板由创建它们的扩展程序拥有。扩展程序必须保留从 createWebviewPanel 返回的 webview。如果您的扩展程序丢失了此引用,即使 webview 仍会继续显示在 VS Code 中,也无法再次访问该 webview。

与文本编辑器一样,用户也可以随时关闭webview面板。当用户关闭webview面板时,webview本身会被销毁。尝试使用已销毁的webview会抛出异常。这意味着上面使用setInterval的示例实际上有一个重要的错误:如果用户关闭面板,setInterval将继续触发,这将尝试更新panel.webview.html,当然这会抛出异常。猫讨厌异常。让我们修复这个问题!

当webview被销毁时,onDidDispose事件会被触发。我们可以使用这个事件来取消进一步的更新并清理webview的资源:

import * as vscode from 'vscode';

const cats = {
  'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
  'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif'
};

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {}
      );

      let iteration = 0;
      const updateWebview = () => {
        const cat = iteration++ % 2 ? 'Compiling Cat' : 'Coding Cat';
        panel.title = cat;
        panel.webview.html = getWebviewContent(cat);
      };

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

      panel.onDidDispose(
        () => {
          // When the panel is closed, cancel any future updates to the webview content
          clearInterval(interval);
        },
        null,
        context.subscriptions
      );
    })
  );
}

扩展程序也可以通过调用dispose()来以编程方式关闭webviews。例如,如果我们想将猫的工作日限制为五秒钟:

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {}
      );

      panel.webview.html = getWebviewContent('Coding Cat');

      // After 5sec, programmatically close the webview panel
      const timeout = setTimeout(() => panel.dispose(), 5000);

      panel.onDidDispose(
        () => {
          // Handle user closing panel before the 5sec have passed
          clearTimeout(timeout);
        },
        null,
        context.subscriptions
      );
    })
  );
}

可见性和移动

当webview面板移动到后台标签页时,它会变为隐藏状态。然而,它不会被销毁。当面板再次被带到前台时,VS Code会自动从webview.html恢复webview的内容:

当webview再次可见时,Webview内容会自动恢复

.visible 属性告诉您网页视图面板当前是否可见。

扩展可以通过调用reveal()以编程方式将webview面板带到前台。此方法接受一个可选的视图列参数,以显示面板。一个webview面板一次只能显示在单个编辑器列中。调用reveal()或将webview面板拖动到新的编辑器列会将webview移动到该新列。

当你在标签之间拖动Webviews时,它们会移动

让我们更新我们的扩展,以仅允许同时存在一个webview。如果面板在后台,那么catCoding.start命令将把它带到前台:

export function activate(context: vscode.ExtensionContext) {
  // Track the current panel with a webview
  let currentPanel: vscode.WebviewPanel | undefined = undefined;

  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const columnToShowIn = vscode.window.activeTextEditor
        ? vscode.window.activeTextEditor.viewColumn
        : undefined;

      if (currentPanel) {
        // If we already have a panel, show it in the target column
        currentPanel.reveal(columnToShowIn);
      } else {
        // Otherwise, create a new panel
        currentPanel = vscode.window.createWebviewPanel(
          'catCoding',
          'Cat Coding',
          columnToShowIn || vscode.ViewColumn.One,
          {}
        );
        currentPanel.webview.html = getWebviewContent('Coding Cat');

        // Reset when the current panel is closed
        currentPanel.onDidDispose(
          () => {
            currentPanel = undefined;
          },
          null,
          context.subscriptions
        );
      }
    })
  );
}

以下是新扩展的实际效果:

使用单个面板和显示

每当webview的可见性发生变化,或者当webview被移动到新列时,onDidChangeViewState事件会被触发。我们的扩展可以使用此事件根据webview显示的列来更改猫:

const cats = {
  'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
  'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif',
  'Testing Cat': 'https://media.giphy.com/media/3oriO0OEd9QIDdllqo/giphy.gif'
};

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {}
      );
      panel.webview.html = getWebviewContent('Coding Cat');

      // Update contents based on view state changes
      panel.onDidChangeViewState(
        e => {
          const panel = e.webviewPanel;
          switch (panel.viewColumn) {
            case vscode.ViewColumn.One:
              updateWebviewForCat(panel, 'Coding Cat');
              return;

            case vscode.ViewColumn.Two:
              updateWebviewForCat(panel, 'Compiling Cat');
              return;

            case vscode.ViewColumn.Three:
              updateWebviewForCat(panel, 'Testing Cat');
              return;
          }
        },
        null,
        context.subscriptions
      );
    })
  );
}

function updateWebviewForCat(panel: vscode.WebviewPanel, catName: keyof typeof cats) {
  panel.title = catName;
  panel.webview.html = getWebviewContent(catName);
}

响应onDidChangeViewState事件

检查和调试webviews

开发者:切换开发者工具命令打开一个开发者工具窗口,您可以使用它来调试和检查您的网页视图。

开发者工具

请注意,如果您使用的是1.56版本之前的VS Code,或者您正在尝试调试设置了enableFindWidget的webview,则必须使用开发者:打开Webview开发者工具命令。此命令为每个webview打开一个专用的开发者工具页面,而不是使用由所有webview和编辑器本身共享的开发者工具页面。

从开发者工具中,您可以使用位于开发者工具窗口左上角的检查工具开始检查您的webview内容:

使用开发者工具检查webview

您还可以在开发者工具控制台中查看来自您的webview的所有错误和日志:

开发者工具控制台

要在您的webview上下文中评估表达式,请确保从开发者工具控制台面板左上角的下拉菜单中选择活动框架环境:

选择活动框架

活动框架环境是webview脚本自身执行的地方。

此外,开发者:重新加载Webview命令会重新加载所有活动的webview。如果您需要重置webview的状态,或者磁盘上的某些webview内容已更改并且您希望加载新内容,这可能会很有帮助。

加载本地内容

Webviews 在隔离的上下文中运行,无法直接访问本地资源。这是出于安全考虑。这意味着,为了从您的扩展中加载图像、样式表和其他资源,或从用户的当前工作区加载任何内容,您必须使用 Webview.asWebviewUri 函数将本地的 file: URI 转换为 VS Code 可以使用的特殊 URI,以加载一部分本地资源。

想象一下,我们希望开始将猫的GIF打包到我们的扩展中,而不是从Giphy拉取。为此,我们首先创建一个指向磁盘上文件的URI,然后通过asWebviewUri函数传递这些URI:

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {}
      );

      // Get path to resource on disk
      const onDiskPath = vscode.Uri.joinPath(context.extensionUri, 'media', 'cat.gif');

      // And get the special URI to use with the webview
      const catGifSrc = panel.webview.asWebviewUri(onDiskPath);

      panel.webview.html = getWebviewContent(catGifSrc);
    })
  );
}

function getWebviewContent(catGifSrc: vscode.Uri) {
  return `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cat Coding</title>
</head>
<body>
    <img src="${catGifSrc}" width="300" />
</body>
</html>`;
}

如果我们调试这段代码,我们会看到catGifSrc的实际值类似于:

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

VS Code 理解这个特殊的 URI,并将使用它从磁盘加载我们的 gif!

默认情况下,webviews 只能访问以下位置的资源:

  • 在您的扩展安装目录内。
  • 在用户当前活动的工作空间内。

使用WebviewOptions.localResourceRoots来允许访问额外的本地资源。

你也可以始终使用数据URI将资源直接嵌入到webview中。

控制对本地资源的访问

Webviews 可以通过 localResourceRoots 选项控制可以从用户机器加载哪些资源。localResourceRoots 定义了一组根 URI,从中可以加载本地内容。

我们可以使用localResourceRoots来限制Cat Coding webviews只能从我们扩展中的media目录加载资源:

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {
          // Only allow the webview to access resources in our extension's media directory
          localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'media')]
        }
      );

      const onDiskPath = vscode.Uri.joinPath(context.extensionUri, 'media', 'cat.gif');
      const catGifSrc = panel.webview.asWebviewUri(onDiskPath);

      panel.webview.html = getWebviewContent(catGifSrc);
    })
  );
}

要禁止所有本地资源,只需将localResourceRoots设置为[]

一般来说,webview 在加载本地资源时应尽可能严格。然而,请记住,localResourceRoots 本身并不提供完全的安全保护。确保您的 webview 也遵循 安全最佳实践,并添加 内容安全策略 以进一步限制可以加载的内容。

主题化网页视图内容

Webview 可以使用 CSS 根据 VS Code 的当前主题更改其外观。VS Code 将主题分为三类,并在 body 元素上添加一个特殊类以指示当前主题:

  • vscode-light - 浅色主题。
  • vscode-dark - 深色主题。
  • vscode-high-contrast - 高对比度主题。

以下CSS根据用户的当前主题更改webview的文本颜色:

body.vscode-light {
  color: black;
}

body.vscode-dark {
  color: white;
}

body.vscode-high-contrast {
  color: red;
}

在开发webview应用程序时,请确保它适用于三种类型的主题。并且始终在高对比度模式下测试您的webview,以确保视力障碍者可以使用它。

Webviews 也可以使用 CSS 变量 访问 VS Code 主题颜色。这些变量名称以 vscode 为前缀,并将 . 替换为 -。例如 editor.foreground 变为 var(--vscode-editor-foreground)

code {
  color: var(--vscode-editor-foreground);
}

查看主题颜色参考以获取可用的主题变量。一个扩展可用,它为变量提供IntelliSense建议。

以下字体相关的变量也被定义:

  • --vscode-editor-font-family - 编辑器字体家族(来自editor.fontFamily设置)。
  • --vscode-editor-font-weight - 编辑器字体粗细(来自editor.fontWeight设置)。
  • --vscode-editor-font-size - 编辑器字体大小(来自editor.fontSize设置)。

最后,对于需要编写针对单个主题的CSS的特殊情况,webviews的body元素有一个名为vscode-theme-id的数据属性,该属性存储当前活动主题的ID。这使您可以为webviews编写特定主题的CSS:

body[data-vscode-theme-id="One Dark Pro"] {
    background: hotpink;
}

支持的媒体格式

Webviews 支持音频和视频,但并非所有媒体编解码器或媒体文件容器类型都受支持。

以下音频格式可以在Webviews中使用:

  • Wav
  • Mp3
  • Ogg
  • Flac

以下视频格式可以在网页视图中使用:

  • H.264
  • VP8

对于视频文件,请确保视频和音频轨道的媒体格式都受支持。例如,许多.mp4文件使用H.264作为视频格式和AAC作为音频格式。VS Code 将能够播放mp4的视频部分,但由于不支持AAC音频,因此不会有任何声音。相反,您需要使用mp3作为音频轨道。

上下文菜单

高级网页视图可以自定义用户右键点击网页视图内部时显示的上下文菜单。这是通过使用贡献点来完成的,类似于VS Code的普通上下文菜单,因此自定义菜单可以很好地与编辑器的其他部分融合。网页视图还可以为网页视图的不同部分显示自定义上下文菜单。

要在你的webview中添加一个新的上下文菜单项,首先在新的webview/context部分下的menus中添加一个新条目。每个贡献需要一个command(这也是项目标题的来源)和一个when子句。when子句应包含webviewId == 'YOUR_WEBVIEW_VIEW_TYPE'以确保上下文菜单仅适用于你的扩展的webviews:

"contributes": {
  "menus": {
    "webview/context": [
      {
        "command": "catCoding.yarn",
        "when": "webviewId == 'catCoding'"
      },
      {
        "command": "catCoding.insertLion",
        "when": "webviewId == 'catCoding' && webviewSection == 'editor'"
      }
    ]
  },
  "commands": [
    {
      "command": "catCoding.yarn",
      "title": "Yarn 🧶",
      "category": "Cat Coding"
    },
    {
      "command": "catCoding.insertLion",
      "title": "Insert 🦁",
      "category": "Cat Coding"
    },
    ...
  ]
}

在webview内部,您还可以使用data-vscode-context 数据属性(或在JavaScript中使用dataset.vscodeContext)为HTML的特定区域设置上下文。data-vscode-context的值是一个JSON对象,它指定了当用户右键单击元素时要设置的上下文。最终的上下文是通过从文档根目录到被点击的元素来确定的。

考虑以下HTML示例:

<div class="main" data-vscode-context='{"webviewSection": "main", "mouseCount": 4}'>
  <h1>Cat Coding</h1>

  <textarea data-vscode-context='{"webviewSection": "editor", "preventDefaultContextMenuItems": true}'></textarea>
</div>

如果用户右键点击textarea,将设置以下上下文:

  • webviewSection == 'editor' - 这会覆盖父元素中的webviewSection
  • mouseCount == 4 - 这是从父元素继承的。
  • preventDefaultContextMenuItems == true - 这是一个特殊的上下文,用于隐藏VS Code通常添加到webview上下文菜单中的复制和粘贴条目。

如果用户在内右键点击,他们将看到:

在webview中显示的自定义上下文菜单

有时在左键/主键点击时显示菜单可能很有用。例如,在分割按钮上显示菜单。你可以通过在onClick事件中派发contextmenu事件来实现这一点:

<button data-vscode-context='{"preventDefaultContextMenuItems": true }' onClick='((e) => {
        e.preventDefault();
        e.target.dispatchEvent(new MouseEvent("contextmenu", { bubbles: true, clientX: e.clientX, clientY: e.clientY }));
        e.stopPropagation();
    })(event)'>Create</button>

带有菜单的分割按钮

脚本和消息传递

Webviews 就像 iframes 一样,这意味着它们也可以运行脚本。默认情况下,JavaScript 在 webviews 中被禁用,但可以通过传入 enableScripts: true 选项轻松重新启用。

让我们使用一个脚本来添加一个计数器,跟踪我们的猫编写的源代码行数。运行一个基本脚本非常简单,但请注意,这个例子仅用于演示目的。实际上,您的webview应始终使用内容安全策略禁用内联脚本:

import * as path from 'path';
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {
          // Enable scripts in the webview
          enableScripts: true
        }
      );

      panel.webview.html = getWebviewContent();
    })
  );
}

function getWebviewContent() {
  return `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cat Coding</title>
</head>
<body>
    <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
    <h1 id="lines-of-code-counter">0</h1>

    <script>
        const counter = document.getElementById('lines-of-code-counter');

        let count = 0;
        setInterval(() => {
            counter.textContent = count++;
        }, 100);
    </script>
</body>
</html>`;
}

在webview中运行的脚本

哇!那真是一只高效的猫。

Webview 脚本可以执行普通网页上的脚本所能执行的几乎所有操作。但请记住,webview 存在于它们自己的上下文中,因此 webview 中的脚本无法访问 VS Code API。这就是消息传递的用武之地!

从扩展传递消息到webview

扩展程序可以使用webview.postMessage()向其webviews发送数据。此方法将任何可JSON序列化的数据发送到webview。消息通过标准的message事件在webview内部接收。

为了演示这一点,让我们向Cat Coding添加一个新命令,该命令指示当前正在编码的猫重构其代码(从而减少总行数)。新的catCoding.doRefactor命令使用postMessage将指令发送到当前的webview,并在webview内部使用window.addEventListener('message', event => { ... })来处理消息:

export function activate(context: vscode.ExtensionContext) {
  // Only allow a single Cat Coder
  let currentPanel: vscode.WebviewPanel | undefined = undefined;

  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      if (currentPanel) {
        currentPanel.reveal(vscode.ViewColumn.One);
      } else {
        currentPanel = vscode.window.createWebviewPanel(
          'catCoding',
          'Cat Coding',
          vscode.ViewColumn.One,
          {
            enableScripts: true
          }
        );
        currentPanel.webview.html = getWebviewContent();
        currentPanel.onDidDispose(
          () => {
            currentPanel = undefined;
          },
          undefined,
          context.subscriptions
        );
      }
    })
  );

  // Our new command
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.doRefactor', () => {
      if (!currentPanel) {
        return;
      }

      // Send a message to our webview.
      // You can send any JSON serializable data.
      currentPanel.webview.postMessage({ command: 'refactor' });
    })
  );
}

function getWebviewContent() {
  return `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cat Coding</title>
</head>
<body>
    <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
    <h1 id="lines-of-code-counter">0</h1>

    <script>
        const counter = document.getElementById('lines-of-code-counter');

        let count = 0;
        setInterval(() => {
            counter.textContent = count++;
        }, 100);

        // Handle the message inside the webview
        window.addEventListener('message', event => {

            const message = event.data; // The JSON data our extension sent

            switch (message.command) {
                case 'refactor':
                    count = Math.ceil(count * 0.5);
                    counter.textContent = count;
                    break;
            }
        });
    </script>
</body>
</html>`;
}

向webview传递消息

从webview向扩展传递消息

Webview 也可以将消息传递回其扩展。这是通过在 webview 内部的一个特殊的 VS Code API 对象上使用 postMessage 函数来实现的。要访问 VS Code API 对象,请在 webview 内部调用 acquireVsCodeApi。此函数在每个会话中只能调用一次。您必须保留此方法返回的 VS Code API 实例,并将其分发给任何需要使用它的其他函数。

我们可以在Cat Coding webview中使用VS Code API和postMessage来在猫在代码中引入错误时提醒扩展:

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {
          enableScripts: true
        }
      );

      panel.webview.html = getWebviewContent();

      // Handle messages from the webview
      panel.webview.onDidReceiveMessage(
        message => {
          switch (message.command) {
            case 'alert':
              vscode.window.showErrorMessage(message.text);
              return;
          }
        },
        undefined,
        context.subscriptions
      );
    })
  );
}

function getWebviewContent() {
  return `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cat Coding</title>
</head>
<body>
    <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
    <h1 id="lines-of-code-counter">0</h1>

    <script>
        (function() {
            const vscode = acquireVsCodeApi();
            const counter = document.getElementById('lines-of-code-counter');

            let count = 0;
            setInterval(() => {
                counter.textContent = count++;

                // Alert the extension when our cat introduces a bug
                if (Math.random() < 0.001 * count) {
                    vscode.postMessage({
                        command: 'alert',
                        text: '🐛  on line ' + count
                    })
                }
            }, 100);
        }())
    </script>
</body>
</html>`;
}

从webview向主扩展传递消息

出于安全原因,您必须将VS Code API对象保持私有,并确保它永远不会泄漏到全局作用域中。

使用Web Workers

Web Workers 在 webviews 中受支持,但有一些重要的限制需要注意。

首先,工作者只能使用data:blob: URI加载。你不能直接从扩展的文件夹中加载工作者。

如果你确实需要从扩展中的JavaScript文件加载工作代码,尝试使用fetch

const workerSource = 'absolute/path/to/worker.js';

fetch(workerSource)
  .then(result => result.blob())
  .then(blob => {
    const blobUrl = URL.createObjectURL(blob);
    new Worker(blobUrl);
  });

Worker脚本也不支持使用importScriptsimport(...)导入源代码。如果你的worker动态加载代码,尝试使用像webpack这样的打包工具将worker脚本打包成一个文件。

使用webpack,你可以使用LimitChunkCountPlugin来强制将编译的worker JavaScript文件合并为一个单一文件:

const path = require('path');
const webpack = require('webpack');

module.exports = {
  target: 'webworker',
  entry: './worker/src/index.js',
  output: {
    filename: 'worker.js',
    path: path.resolve(__dirname, 'media')
  },
  plugins: [
    new webpack.optimize.LimitChunkCountPlugin({
      maxChunks: 1
    })
  ]
};

安全

与任何网页一样,在创建webview时,您必须遵循一些基本的安全最佳实践。

限制功能

一个webview应该具备其所需的最小功能集。例如,如果你的webview不需要运行脚本,就不要设置enableScripts: true。如果你的webview不需要从用户的工作区加载资源,可以将localResourceRoots设置为[vscode.Uri.file(extensionContext.extensionPath)],甚至设置为[]以禁止访问所有本地资源。

内容安全策略

内容安全策略进一步限制了可以在webview中加载和执行的内容。例如,内容安全策略可以确保只有允许的脚本列表才能在webview中运行,甚至可以告诉webview只通过https加载图像。

要添加内容安全策略,请在webview的 顶部放置一个指令

function getWebviewContent() {
  return `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">

    <meta http-equiv="Content-Security-Policy" content="default-src 'none';">

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>Cat Coding</title>
</head>
<body>
    ...
</body>
</html>`;
}

策略 default-src 'none'; 禁止所有内容。然后我们可以重新启用扩展功能所需的最少内容。以下是一个内容安全策略,允许加载本地脚本和样式表,并通过 https 加载图像:

<meta
  http-equiv="Content-Security-Policy"
  content="default-src 'none'; img-src ${webview.cspSource} https:; script-src ${webview.cspSource}; style-src ${webview.cspSource};"
/>

${webview.cspSource} 值是一个占位符,表示来自 webview 对象本身的值。有关如何使用此值的完整示例,请参阅 webview 示例

此内容安全策略还隐式禁用了内联脚本和样式。最佳实践是将所有内联样式和脚本提取到外部文件中,以便在不放宽内容安全策略的情况下正确加载它们。

仅通过https加载内容

如果您的webview允许加载外部资源,强烈建议您只允许这些资源通过https加载,而不是通过http加载。上面的示例内容安全策略已经通过只允许通过https:加载图像来实现这一点。

清理所有用户输入

就像您为普通网页所做的那样,在构建webview的HTML时,您必须对所有用户输入进行清理。未能正确清理输入可能会导致内容注入,这可能使您的用户面临安全风险。

必须进行清理的示例值:

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

考虑使用辅助库来构建您的HTML字符串,或至少确保用户工作空间中的所有内容都经过适当的清理。

切勿仅依赖消毒来确保安全。确保遵循其他安全最佳实践,例如制定内容安全策略,以尽量减少任何潜在内容注入的影响。

持久性

在标准的webview 生命周期中,webviews由createWebviewPanel创建,并在用户关闭它们或调用.dispose()时销毁。然而,webview的内容在webview变为可见时创建,并在webview移动到后台时销毁。当webview移动到后台标签页时,webview内的任何状态都将丢失。

解决这个问题的最佳方法是使您的webview无状态。使用消息传递来保存webview的状态,然后在webview再次可见时恢复状态。

getState 和 setState

在webview内部运行的脚本可以使用getStatesetState方法来保存和恢复一个可JSON序列化的状态对象。即使webview内容本身在webview面板隐藏时被销毁,这个状态仍然会被保留。当webview面板被销毁时,这个状态也会被销毁。

// Inside a webview script
const vscode = acquireVsCodeApi();

const counter = document.getElementById('lines-of-code-counter');

// Check if we have an old state to restore from
const previousState = vscode.getState();
let count = previousState ? previousState.count : 0;
counter.textContent = count;

setInterval(() => {
  counter.textContent = count++;
  // Update the saved state
  vscode.setState({ count });
}, 100);

getStatesetState 是持久化状态的首选方法,因为它们的性能开销比 retainContextWhenHidden 低得多。

序列化

通过实现WebviewPanelSerializer,当VS Code重新启动时,您的webviews可以自动恢复。序列化基于getStatesetState,并且只有在您的扩展为您的webviews注册了WebviewPanelSerializer时才会启用。

为了使我们的编码猫在VS Code重启后仍然存在,首先在扩展的package.json中添加一个onWebviewPanel激活事件:

"activationEvents": [
    ...,
    "onWebviewPanel:catCoding"
]

此激活事件确保每当VS Code需要恢复具有viewType: catCoding的webview时,我们的扩展将被激活。

然后,在我们的扩展的activate方法中,调用registerWebviewPanelSerializer来注册一个新的WebviewPanelSerializerWebviewPanelSerializer负责从其持久化状态恢复webview的内容。这个状态是webview内容使用setState设置的JSON blob。

export function activate(context: vscode.ExtensionContext) {
  // Normal setup...

  // And make sure we register a serializer for our webview type
  vscode.window.registerWebviewPanelSerializer('catCoding', new CatCodingSerializer());
}

class CatCodingSerializer implements vscode.WebviewPanelSerializer {
  async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel, state: any) {
    // `state` is the state persisted using `setState` inside the webview
    console.log(`Got state: ${state}`);

    // Restore the content of our webview.
    //
    // Make sure we hold on to the `webviewPanel` passed in here and
    // also restore any event listeners we need on it.
    webviewPanel.webview.html = getWebviewContent();
  }
}

现在,如果您在打开猫编程面板的情况下重新启动VS Code,面板将自动恢复到相同的编辑器位置。

retainContextWhenHidden

对于具有非常复杂的用户界面或无法快速保存和恢复状态的网页视图,您可以使用retainContextWhenHidden选项。此选项使网页视图即使在不再处于前台时,也能保持其内容但处于隐藏状态。

尽管Cat Coding很难说具有复杂的状态,但让我们尝试启用retainContextWhenHidden,看看这个选项如何改变webview的行为:

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {
          enableScripts: true,
          retainContextWhenHidden: true
        }
      );
      panel.webview.html = getWebviewContent();
    })
  );
}

function getWebviewContent() {
  return `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cat Coding</title>
</head>
<body>
    <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
    <h1 id="lines-of-code-counter">0</h1>

    <script>
        const counter = document.getElementById('lines-of-code-counter');

        let count = 0;
        setInterval(() => {
            counter.textContent = count++;
        }, 100);
    </script>
</body>
</html>`;
}

retainContextWhenHidden 演示

请注意,当webview隐藏并恢复时,计数器现在不会重置。不需要额外的代码!使用retainContextWhenHidden,webview的行为类似于网页浏览器中的后台标签页。即使标签页不活动或不可见,脚本和其他动态内容也会继续运行。当启用retainContextWhenHidden时,您还可以向隐藏的webview发送消息。

尽管retainContextWhenHidden可能很吸引人,但请记住,这会带来很高的内存开销,应该只在其他持久化技术无法工作时使用。

可访问性

当用户使用屏幕阅读器操作VS Code时,类vscode-using-screen-reader将被添加到您的webview的主体中。此外,如果用户表达了减少窗口中运动量的偏好,类vscode-reduce-motion将被添加到文档的主体元素中。通过观察这些类并相应地调整您的渲染,您的webview内容可以更好地反映用户的偏好。

下一步

如果您想了解更多关于VS Code扩展性的信息,请尝试以下主题: