网页视图API

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

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

Webview 在多个 VS Code API 中使用:

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

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

VS Code API 使用

我应该使用网络视图吗?

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

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

  • 此功能真的需要存在于 VS Code 中吗?作为单独的应用程序或网站会更好吗?

  • Webview 是实现您的功能的唯一方法吗?您可以改用常规 VS Code API 吗?

  • 您的 webview 会增加足够的用户价值来证明其高资源成本的合理性吗?

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

Webview API 基础知识

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

这是Cat Codingpackage.json扩展的第一个版本。您可以在此处找到示例应用程序的完整代码。我们扩展的第一个版本提供了一个名为 的命令。当用户调用此命令时,我们将显示一个简单的 Web 视图,其中包含我们的猫。用户将能够从命令面板调用此命令作为Cat 编码:启动新的 cat 编码会话,甚至如果愿意的话,甚至可以为其创建一个键绑定。catCoding.start

{
  "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 版本,则必须onCommand:catCoding.startactivationEvents.

现在我们来执行该catCoding.start命令。在我们的扩展的主文件中,我们注册catCoding.start命令并使用它来显示基本的网络视图:

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函数在编辑器中创建并显示一个网络视图。catCoding.start如果您尝试在当前状态下运行该命令,您会看到以下内容:

一个空的网页视图

我们的命令打开一个新的 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>`;
}

如果再次运行该命令,现在 Web 视图如下所示:

带有一些 HTML 的 webview

进步!

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

更新网页视图内容

webview.html还可以在创建 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);
      };

      // 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.html替换整个 webview 内容,类似于重新加载 iframe。一旦开始在 Web 视图中使用脚本,记住这一点很重要,因为这意味着设置webview.html还会重置脚本的状态。

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

生命周期

Webview 面板由创建它们的扩展拥有。扩展程序必须保留从 返回的 webview createWebviewPanel。如果您的扩展程序丢失了此引用,则它无法再次重新获得对该 Web 视图的访问权限,即使该 Web 视图将继续显示在 VS Code 中。

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

onDidDispose当 webview 被销毁时会触发该事件。我们可以使用此事件取消进一步更新并清理 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
      );
    })
  );
}

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

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

可见性和移动

当 Web 视图面板移至背景选项卡时,它会被隐藏。但它并没有被破坏。webview.html当面板再次进入前台时,VS Code 将自动恢复 webview 的内容:

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

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

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

当您在选项卡之间拖动 Web 视图时,它们会移动

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

这是正在运行的新扩展:

使用单个面板并显示

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

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 事件

检查和调试网络视图

开发人员:切换开发人员工具命令会打开一个开发人员工具窗口,您可以使用该窗口进行调试并检查您的 Web 视图。

开发者工具

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

在开发人员工具中,您可以使用位于开发人员工具窗口左上角的检查工具开始检查 Web 视图的内容:

使用开发者工具检查 webview

您还可以在开发人员工具控制台中查看 Web 视图中的所有错误和日志:

开发者工具控制台

要在 Web 视图上下文中计算表达式,请确保从开发人员工具控制台面板左上角的下拉列表中选择活动框架环境:

选择活动框架

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

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

加载本地内容

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允许访问其他本地资源。

您还可以始终使用数据 URI 将资源直接嵌入到 Web 视图中。

控制对本地资源的访问

Webview 可以通过选项控制可以从用户计算机加载哪些资源localResourceRootslocalResourceRoots定义一组可以从中加载本地内容的根 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 可以使用 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 还可以使用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。这使您可以为 webview 编写特定于主题的 CSS:

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

支持的媒体格式

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

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

  • 波形
  • mp3
  • 奥格
  • 弗拉克

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

  • H.264
  • VP8

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

上下文菜单

高级 Web 视图可以自定义用户在 Web 视图内部右键单击时显示的上下文菜单。这是使用类似于 VS Code 的正常上下文菜单的贡献点来完成的,因此自定义菜单可以与编辑器的其余部分完美契合。Web 视图还可以显示 Web 视图不同部分的自定义上下文菜单。

要向 Web 视图添加新的上下文菜单项,请首先在menuswebview/context部分下添加一个新条目。每个贡献都需要一个command(这也是项目标题的来源)和一个when子句。应包含when 子句webviewId == 'YOUR_WEBVIEW_VIEW_TYPE'确保上下文菜单仅适用于您的扩展程序的 Web 视图:

"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 data 属性(或在 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>,他们将看到:

在网络视图中显示自定义上下文菜单

有时,在左键/主单击上显示菜单可能很有用。例如,在拆分按钮上显示菜单。您可以通过contextmenuonClick事件中分派事件来完成此操作:

<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();
    }}>Create</button>

带菜单的拆分按钮

脚本和消息传递

Webview 就像 iframe 一样,这意味着它们也可以运行脚本。默认情况下,Web 视图中 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>`;
}

在网络视图中运行的脚本

哇!那是一只多产的猫。

Webview 脚本几乎可以执行普通网页上的脚本可以执行的任何操作。请记住,尽管 Web 视图存在于其自己的上下文中,因此 Web 视图中的脚本无法访问 VS Code API。这就是消息传递的用武之地!

将消息从扩展传递到 Web 视图

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

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

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

将消息传递到网络视图

将消息从 Web 视图传递到扩展

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

postMessage当我们的猫在代码中引入错误时,我们可以使用 VS Code API 和Cat Coding Web 视图来向扩展发出警报:

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在 webview 内部受支持,但有一些重要的限制需要注意。

首先,worker 只能使用 adata: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 脚本也不支持使用importScripts或导入源代码import(...)如果您的工作程序动态加载代码,请尝试使用webpack等捆绑程序将工作程序脚本打包到单个文件中。

使用webpack,您可以使用LimitChunkCountPlugin强制编译后的辅助 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
    })
  ]
};

安全

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

限制能力

Webview 应该具有它所需的最小功能集。例如,如果您的 webview 不需要运行脚本,则不要设置enableScripts: true. 如果您的 webview 不需要从用户工作区加载资源,请设置localResourceRoots[vscode.Uri.file(extensionContext.extensionPath)]甚至[]禁止访问所有本地资源。

内容安全政策

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

要添加内容安全策略,请将<meta http-equiv="Content-Security-Policy">指令放在 webview 的顶部<head>

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允许加载外部资源,强烈建议你只允许通过http加载这些资源https,而不是通过http加载。上面的示例内容安全策略已经通过仅允许加载图像来做到这一点https:

清理所有用户输入

就像处理普通网页一样,在为 webview 构建 HTML 时,必须清理所有用户输入。未能正确清理输入可能会允许内容注入,这可能会让您的用户面临安全风险。

必须清理的示例值:

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

考虑使用帮助程序库来构建 HTML 字符串,或者至少确保用户工作区中的所有内容都经过正确清理。

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

坚持

在标准的 webview生命周期createWebviewPanel中,webview在用户关闭或.dispose()调用时创建并销毁。然而,webview 的内容是在 webview 变得可见时创建的,并在 webview 移入后台时销毁的。当 Web 视图移动到后台选项卡时,Web 视图内的任何状态都将丢失。

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

获取状态和设置状态

在 webview 中运行的脚本可以使用getStatesetState方法来保存和恢复 JSON 可序列化状态对象。当 Web 视图面板隐藏时,即使 Web 视图内容本身被破坏,此状态也会持续存在。当 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,您的 webview 可以在 VS Code 重新启动时自动恢复。序列化建立在getState和之上setState,并且仅当您的扩展程序WebviewPanelSerializer为您的网络视图注册了 时才启用。

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

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

此激活事件确保每当 VS Code 需要恢复具有 viewType: 的 Web 视图时,我们的扩展都会被激活catCoding

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

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();
  }
}

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

隐藏时保留上下文

对于具有非常复杂的 UI 或状态且无法快速保存和恢复的 Web 视图,您可以使用该retainContextWhenHidden选项。此选项使 Web 视图保留其内容,但处于隐藏状态,即使 Web 视图本身不再位于前台也是如此。

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

持久性再训练

请注意,当 Web 视图隐藏然后恢复时,计数器现在不会重置。无需额外代码!使用 时retainContextWhenHidden,网络视图的行为类似于网络浏览器中的后台选项卡。脚本和其他动态内容被暂停,但一旦 Web 视图再次可见,就会立即恢复。retainContextWhenHidden即使启用,您也无法将消息发送到隐藏的 Web 视图。

尽管retainContextWhenHidden可能很有吸引力,但请记住,这具有较高的内存开销,并且仅应在其他持久性技术不起作用时才使用。

无障碍

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

下一步

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