现已发布!阅读 10 月份的新功能和修复。

Webview API

webview API 允许扩展在 Visual Studio Code 中创建完全可自定义的视图。例如,内置的 Markdown 扩展使用 webview 呈现 Markdown 预览。Webview 还可以用于构建比 VS Code 的本机 API 支持更复杂的用户界面。

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

Webview 用于多个 VS Code API 中

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

此页面重点介绍基本的 webview 面板 API,尽管这里介绍的大多数内容也适用于自定义编辑器和 webview 视图中使用的 webview。即使您更感兴趣的是这些 API,我们建议您先通读此页面,以便熟悉 webview 的基础知识。

VS Code API 用法

我应该使用 webview 吗?

Webview 非常棒,但它们也应该谨慎使用,并且只有在 VS Code 的本机 API 不足时才使用。Webview 资源占用量大,并且在与普通扩展不同的上下文中运行。设计不当的 webview 也容易让人感觉与 VS Code 格格不入。

在使用 webview 之前,请考虑以下几点

  • 此功能是否真的需要在 VS Code 中运行?作为独立应用程序或网站是否更好?

  • webview 是实现您的功能的唯一方法吗?您能否使用常规的 VS Code API?

  • 您的 webview 是否会增加足够的价值以抵消其高资源成本?

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

Webviews API 基础知识

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

以下是 Cat Coding 扩展的第一个版本的 package.json。您可以在 此处 找到示例应用程序的完整代码。我们扩展的第一个版本 贡献了一个名为 catCoding.start 的命令。当用户调用此命令时,我们将显示一个带有我们猫的简单 webview。用户可以从 命令面板 中调用此命令(名为 Cat Coding: Start new cat coding session),或者如果他们愿意,甚至可以为此命令创建键盘绑定。

{
  "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 命令,您将看到以下内容

An empty 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 将如下所示

A webview with some HTML

进度!

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

更新 webview 内容

webview.html 也可以在 webview 创建后更新 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>`;
}

Updating the webview content

设置 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() 来以编程方式关闭 webview。例如,如果我们想将我们猫的工作时间限制在五秒钟内

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 content is automatically restored when the webview becomes visible again

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

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

Webviews are moved when you drag them between tabs

让我们更新我们的扩展,以一次只允许存在一个 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
        );
      }
    })
  );
}

以下是新扩展的实际操作

Using a single panel and reveal

每当 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);
}

Responding to onDidChangeViewState events

检查和调试 webview

开发人员:切换开发者工具 命令会打开一个 开发者工具 窗口,您可以在其中调试和检查您的 webview。

The developer tools

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

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

Inspecting a webview using the developer tools

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

The developer tools console

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

Selecting the active frame

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

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

加载本地内容

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

想象一下,我们希望将猫 GIF 图直接打包到扩展程序中,而不是从 Giphy 获取。为此,我们首先创建指向磁盘上文件的 URI,然后将这些 URI 传递给 asWebviewUri 函数。

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 图!

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

  • 扩展程序的安装目录。
  • 用户当前活动的工作区。

使用 WebviewOptions.localResourceRoots 允许访问其他本地资源。

你也可以始终使用 data URI 将资源直接嵌入 Webview 中。

控制对本地资源的访问

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

我们可以使用 localResourceRoots 来限制 Cat Coding Webview 只能从扩展程序中的 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 内容主题

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,以确保视障人士能够使用它。

Webview 还可以使用 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 的特殊情况,Webview 的 body 元素有一个名为 vscode-theme-id 的数据属性,用于存储当前活动主题的 ID。这使你能够为 Webview 编写特定于主题的 CSS。

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

支持的媒体格式

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

以下音频格式可以在 Webview 中使用

  • Wav
  • Mp3
  • Ogg
  • Flac

以下视频格式可以在 Webview 中使用

  • H.264
  • VP8

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

上下文菜单

高级 Webview 可以自定义用户在 Webview 中右键单击时显示的上下文菜单。这与 VS Code 的普通上下文菜单类似,使用 贡献点 实现,因此自定义菜单可以与编辑器中的其他菜单完美地集成在一起。Webview 还可以为 Webview 的不同部分显示自定义上下文菜单。

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

"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 上下文菜单中的复制和粘贴条目。

如果用户在 <textarea> 内右键单击,他们将看到

Custom context menus showing in a 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>

Split button with a menu

脚本和消息传递

Webview 就像 iframe 一样,这意味着它们也可以运行脚本。默认情况下,Webview 中的 JavaScript 是禁用的,但可以通过传递 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>`;
}

A script running in a webview

哇!这只猫真是太能干了。

Webview 脚本几乎可以做普通网页上的脚本可以做的任何事情。但请记住,Webview 存在于自己的上下文中,因此 Webview 中的脚本无法访问 VS Code API。这就是消息传递的用武之地!

从扩展程序向 Webview 传递消息

扩展程序可以使用 webview.postMessage() 向其 Webview 发送数据。此方法将任何可 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>`;
}

Passing messages to a webview

从 Webview 向扩展程序传递消息

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

我们可以使用 VS Code API 和 postMessage 在我们的 Cat Coding Webview 中,在我们的猫在代码中引入错误时向扩展程序发出警报。

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>`;
}

Passing messages from the webview to the main extension

出于安全原因,你必须将 VS Code API 对象保持私有,并确保它永远不会泄漏到全局范围内。

使用 Web Worker

Web Worker 在 Webview 中受支持,但需要注意一些重要的限制。

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

如果你确实需要从扩展程序中的 JavaScript 文件中加载 Worker 代码,请尝试使用 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 的 <head> 的顶部放置 <meta http-equiv="Content-Security-Policy"> 指令

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 生命周期 中,webview 由 `createWebviewPanel` 创建,并在用户关闭它们或调用 `dispose()` 时销毁。但是,webview 的内容是在 webview 可见时创建的,并在 webview 转移到后台时销毁。当 webview 被移到后台选项卡时,webview 中的任何状态都将丢失。

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

getState 和 setState

在 webview 中运行的脚本可以使用 `getState` 和 `setState` 方法来保存和恢复 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);

`getState` 和 `setState` 是保留状态的首选方法,因为它们比 `retainContextWhenHidden` 的性能开销要低得多。

序列化

通过实现 `WebviewPanelSerializer`,你的 webview 可以自动在 VS Code 重新启动时恢复。序列化建立在 `getState` 和 `setState` 的基础上,并且仅在你的扩展程序为你的 webview 注册了 `WebviewPanelSerializer` 时才会启用。

为了使我们的编码猫在 VS Code 重新启动后仍然存在,首先将 `onWebviewPanel` 激活事件添加到扩展程序的 `package.json` 中

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

此激活事件确保我们的扩展程序将在 VS Code 需要使用 viewType: `catCoding` 恢复 webview 时被激活。

然后,在扩展程序的 `activate` 方法中,调用 `registerWebviewPanelSerializer` 来注册一个新的 `WebviewPanelSerializer`。`WebviewPanelSerializer` 负责从其持久状态恢复 webview 的内容。此状态是 webview 内容使用 `setState` 设置的 JSON 块。

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

对于 UI 或状态非常复杂的 webview,无法快速保存和恢复,你可以使用 `retainContextWhenHidden` 选项。此选项使 webview 保留其内容,但处于隐藏状态,即使 webview 本身不再处于前台也是如此。

虽然 **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 demo

请注意,当 webview 隐藏然后恢复时,计数器不会重置。无需额外代码!使用 `retainContextWhenHidden`,webview 的行为类似于 Web 浏览器中的后台选项卡。即使选项卡未处于活动状态或不可见,脚本和其他动态内容也会继续运行。你也可以在启用 `retainContextWhenHidden` 时向隐藏的 webview 发送消息。

虽然 `retainContextWhenHidden` 可能很有吸引力,但请记住,这会产生很高的内存开销,并且应仅在其他持久性技术无法使用时使用。

辅助功能

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

下一步

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