本站点文档内容均翻译自code.visualstudio.com,仅供个人学习,如有差异请以官网为准。

Webview API

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

可以把网页视图看作是iframe在你的扩展控制的VS Code中。Web视图几乎可以渲染该帧内的任何HTML内容,并通过消息传递与扩展通信。这种自由让网页视图极具威力,也开启了全新的扩展可能性。

Web视图被用于多个VS Code API:

  • 使用以下方式创建的Webview面板createWebviewPanel.在这种情况下,Webview 面板在 VS Code 中被显示为独立的编辑器。这使得它们在展示自定义界面和自定义可视化方面非常有用。
  • 作为自定义编辑器的视图。自定义编辑器允许扩展为编辑工作区中任何文件提供自定义界面。自定义编辑器 API 还允许你的扩展钩入编辑事件,如撤销和重做,以及文件事件如保存。
  • 在侧边栏或面板区域渲染的Webview视图中。详情请参见webview视图示例扩展

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

VS Code API 的使用

我应该用WebView吗?

Web视图确实很棒,但也应该谨慎使用,只有在VS Code原生API不足时才会使用。Web视图资源消耗大,运行环境与普通扩展不同。设计不良的网页视图在 VS Code 中也很容易显得格格不入。

在使用网页视图之前,请考虑以下几点:

  • 这个功能真的需要存在于 VS Code 里吗?作为单独的应用还是网站更好?

  • Web视图是实现你功能的唯一方式吗?你能用普通的 VS Code API 吗?

  • 你的WebView能否为其高资源成本带来足够的用户价值?

记住:仅仅因为你能用webviews做某件事,并不意味着你应该这么做。不过,如果你有信心需要使用webviews,那么这份文档可以帮你。我们开始吧。

Webviews API 基础

为了解释Webview API,我们将构建一个简单的扩展,叫做Cat Coding。这个扩展会用网页视图显示一只猫写代码的GIF(大概是在VS Code里)。在我们开发 API 的过程中,我们会继续为扩展添加功能,包括一个计数器,用来记录猫写了多少行源代码,以及通知用户猫引入错误的通知。

这是package.json用于Cat Coding扩展的第一个版本。你可以在这里找到示例应用的完整代码。我们扩展的第一个版本贡献了一个命令,称为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.start激活事件.

现在让我们实现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指挥部现状:

一个空白的网页视图

我们的命令会打开一个新的网页视图面板,标题正确,但没有任何内容!要将猫添加到新面板,我们还需要用以下方式设置网页视图的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>`;
}

如果你再次执行该命令,网页视图现在看起来是这样的:

带有一些HTML的网页视图

进步!

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

更新网页浏览内容

webview.html也可以在网页视图创建后更新其内容。我们用这个方法让猫编码更具动态性,引入猫轮换:

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。当你开始在Webview中使用脚本时,这一点非常重要,因为这意味着设置webview.html还会重置脚本的状态。

上述例子也使用webview.title以更改编辑器中显示的文档标题。设置标题不会导致网页视图重新加载。

生命周期

Webview 面板归创建它们的扩展所有。扩展必须保留返回的网页视图createWebviewPanel.如果你的扩展失去了这个引用,它就无法重新获得对该网页视图的访问权限,尽管网页视图会在 VS Code 中继续显示。

与文本编辑器一样,用户也可以随时关闭网页视图面板。当用户关闭网页视图面板时,网页视图本身也会被销毁。尝试使用被毁的网页视图会触发异常。这意味着上述例子使用setInterval实际上有一个重要的bug:如果用户关闭面板,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
      );
    })
  );
}

扩展还可以通过调用程序关闭网页视图处置()在他们身上。例如,如果我们想限制猫咪的工作时间为五秒:

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

可见性与移动

当网页视图面板移入背景标签页时,它会被隐藏。不过它并未被摧毁。VS Code 会自动恢复 webview 的内容从webview.html当画面再次被带到前景时:

当网页视图重新可见时,内容会自动恢复

.可见属性会告诉你Webview面板当前是否可见。

扩展可以通过调用程序化地将网页视图面板推到前景揭示().该方法采用可选的目标视图列来显示面板。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
        );
      }
    })
  );
}

以下是新扩展的实际应用:

使用单格并揭示

每当网页视图的可见性发生变化,或网页视图被移入新列时,onDidChangeViewState事件被触发。我们的扩展可以利用此事件根据网页视图显示的列来更改猫咪:

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

检查和调试网页视图

开发者:切换开发者工具命令会打开一个开发者工具Windows,你可以使用它进行调试和检查你的网页视图。

开发者工具

请注意,如果你使用的是1.56之前的VS Code版本,或者你试图调试一个设置enableFindWidget你必须使用“开发者:打开Webview Developer Tools”命令。该命令为每个网页视图打开专用的开发者工具页面,而不是使用所有网页视图和编辑器共享的开发者工具页面。

在开发者工具中,你可以使用位于开发者工具Windows左上角的检查工具开始检查网页视图的内容:

使用开发者工具检查网页视图

你也可以在开发者工具控制台中查看网页视图中的所有错误和日志:

开发者工具控制台

要在网页视图中评估表达式,请务必从开发者工具控制台面板左上角的下拉菜单中选择活跃帧环境:

选择活动帧

主动帧环境是Webview脚本本身的执行场所。

此外,“开发者:重新加载 Webview” 命令会重新加载所有活跃的 WebView。如果你需要重置 webview 状态,或者磁盘上的 webview 内容发生变化,需要加载新内容,这会很有帮助。

加载本地内容

Web视图运行在孤立的环境中,无法直接访问本地资源。这样做是为了安全考虑。这意味着要从你的扩展中加载图片、样式表和其他资源,或加载用户当前工作区的任何内容,你必须使用Webview.asWebviewUri函数 用于转换 本地文件:URI 转换为一个特殊 URI,VS Code 可以用它加载部分本地资源。

想象一下,我们想把猫咪动图打包到扩展里,而不是从Giphy里拿出来。为此,我们首先为磁盘文件创建一个URI,然后将这些URI传递给作为WebviewUri功能:

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直接嵌入资源到Webview中。

控制本地资源的访问

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

我们可以使用localResourceRoots限制Cat编码的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,
        {
          // 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 可以用 CSS 根据 VS Code 当前主题改变它们的外观。VS Code 将主题分为三类,并为身体表示当前主题的元素:

  • VScode-Light- 轻音乐主题。
  • VScode-暗- 黑暗主题。
  • VScode-高对比度- 高对比度主题。

以下 CSS 根据用户当前主题更改网页视图的文本颜色:

body.vscode-light {
  color: black;
}

body.vscode-dark {
  color: white;
}

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

在开发Webview应用时,确保它能支持这三种主题类型。并且务必测试高对比度模式,确保视障人士也能使用。

Web视图还可以通过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(对应代码编辑器-font-weight)- 编辑器字体权重(来自editor.fontWeight(编辑者)字体权重环境)。
  • --vscode-editor-font-size- 编辑器字体大小(来自editor.fontSize环境)。

最后,对于需要编写针对单一主题的CSS的特殊情况,webviews的正体元素有一个称为data属性vscode-theme-id该主题存储当前活跃主题的ID。这让你可以为WebView编写主题特定的CSS:

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

支持的媒体格式

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

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

  • Wav
  • MP3
  • 奥格
  • Flac

以下视频格式可用于网页视图:

  • H.264
  • VP8

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

上下文菜单

高级网页视图可以自定义用户右键点击网页视图时显示的上下文菜单。这通过贡献实现,类似于VS Code的常规上下文菜单,因此自定义菜单与编辑器的其他部分紧密结合。Web视图还可以为网页视图的不同部分显示自定义上下文菜单。

要向网页视图添加新的上下文菜单项,首先在菜单在新Webview/背景分区。每个贡献取指挥(这也是该物品标题的来源)以及一个条款。“当时”条款应包括webviewID == 'YOUR_WEBVIEW_VIEW_TYPE'为了确保上下文菜单只适用于你扩展的网页视图:

"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"
    },
    ...
  ]
}

在网页视图内,你还可以使用数据与代码上下文 data属性(或在JavaScript中为dataset.vscodeContext).该数据与代码上下文value 是一个 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>

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

  • webviewSection == 'editor'- 此覆盖网页视图部分来自母元素。
  • 鼠标计数 == 4- 这是继承自父元素的。
  • preventDefaultContextMenuItems == true- 这是一个特殊的上下文,隐藏了 VS Code 通常添加到 webview 上下文菜单的复制粘贴条目。

如果用户在<textarea>,他们将看到:

网页视图中显示的自定义上下文菜单

有时候在左键或主键上显示菜单会很有用。比如,在分屏按钮上显示菜单。你可以通过调度上下文菜单事件发生在onClick事件:

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

带菜单的分键

脚本与消息传递

Web视图就像iframe一样,意味着它们也能运行脚本。JavaScript 默认在 webview 中被禁用,但可以通过传递enableScripts: true选项。

我们用脚本添加计数器,追踪猫写的源代码行数。运行一个基本脚本相当简单,但请注意,这个示例仅供演示目的。实际上,你的网页视图应始终使用内容安全策略禁用内联脚本:

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 脚本几乎可以做普通网页脚本能做的所有事情。但请记住,webviews存在于独立的上下文中,因此webview中的脚本无法访问VS Code API。这就是消息传递发挥作用的地方!

将消息从扩展传递到网页视图

扩展可以通过以下方式向其 webview 发送数据webview.postMessage().该方法将任何可序列化的JSON数据发送到WebView。消息通过标准在WebView内部接收信息活动。

为了演示这一点,我们给Cat Coding 添加一个新命令,指示当前编码的cat重构代码(从而减少总行数)。新的catCoding.doRefactor命令的使用帖子信息将指令发送到当前的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>`;
}

将消息传递到网页视图

将消息从webview传递到扩展

Web视图还可以将消息反馈给其扩展。这是通过帖子信息在webview内的特殊VS Code API对象上函数。要访问 VS Code API 对象,请acquireVsCodeApi在网页视图内。该函数每个会话只能调用一次。你必须保留该方法返回的VS Code API实例,并分发给其他需要使用它的函数。

我们可以使用 VS Code API 和帖子信息在我们的猫编码网页视图中,当猫在代码中引入错误时提醒扩展:

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 对象保密,并确保它永远不会泄露到全局作用域。

使用网页工作者

WebViews支持WebWork用户,但有一些重要的限制需要注意。

首先,工人只能通过其中一种数据:Blob:URI。你不能直接从扩展文件夹加载工作者。

如果你确实需要从扩展中的JavaScript文件加载工作代码,可以试试用取球:

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

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

工作脚本也不支持通过以下方式导入源代码importScripts重要性(...).如果你的 worker 是动态加载代码,可以试试用 webpack 这样的捆绑工具把 worker 脚本打包成一个文件。

其中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
    })
  ]
};

安全性

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

极限能力

网页视图应具备其所需的最低功能集。例如,如果你的网页视图不需要运行脚本,就不要设置enableScripts: true.如果你的网页视图不需要从用户工作区加载资源,设置localResourceRoots[VScode。Uri.file(extensionContext.extensionPath)]甚至禁止访问所有本地资源。[]

内容安全政策

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

要添加内容安全策略,请设置<meta http-equiv=“Content-Security-Policy”>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>`;
}

政策内容默认词 “无”;禁止所有内容。然后我们可以重新启用扩展所需的最低内容量。这里有一个内容安全策略,允许加载本地脚本和样式表,并加载图像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}value 是 WebView 对象本身的值的占位符。请参阅webview示例,完整示例如何使用该值。

该内容安全策略还隐式禁用了内联脚本和样式。最佳实践是将所有内嵌样式和脚本提取到外部文件,以便它们能够正确加载,同时不放松内容安全策略。

只通过HTTPS加载内容

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

净化所有用户输入

就像普通网页一样,在构建网页视图的 HTML 时,必须对所有用户输入进行净化处理。未能妥善净化输入可能导致内容注入,从而使用户面临安全风险。

必须净化的示例数值:

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

考虑使用辅助库来构建你的HTML字符串,或者至少确保用户工作区中的所有内容都经过妥善净化。

绝不要仅依赖消毒来保证安全。务必遵循其他安全最佳实践,比如制定内容安全策略,以最大限度减少潜在内容注入的影响。

持久性

在标准的网页视图生命周期中,web视图由createWebviewPanel而当使用者关闭或当.dispose()被叫去。然而,当网页视图变得可见时,webview的内容会被创建;当webview被移到后台时,内容会被销毁。当网页视图被移动到后台标签页时,任何状态都会丢失。

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

getState 和 setState

运行在 webview 中的脚本可以使用getState以及setState用于保存和恢复可序列化的JSON状态对象的方法。即使网页视图面板被隐藏,网页内容本身被销毁后,这种状态依然存在。当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是持久状态的首选方式,因为它们的性能开销远低于retainContextWhHidden.

序列化

通过实现WebviewPanelSerializer,当VS Code重启时,你的webview可以自动恢复。序列化是在getState以及setState,且只有当你的分机注册为WebviewPanelSerializer为了你的网页浏览量。

为了让我们的编码猫在 VS Code 重启后能持续存在,首先添加一个onWebviewPanel激活事件到扩展的package.json:

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

该激活事件确保每当 VS Code 需要恢复带有 viewType: 的 webview 时,我们的扩展都会被激活:猫编码.

然后,在我们的扩展中启动方法,调用registerWebviewPanelSerializer注册新WebviewPanelSerializer.该WebviewPanelSerializer负责恢复Webview内容从持久状态中恢复。该状态是网页视图内容所用的 JSON blobsetState.

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,面板会自动恢复到相同的编辑器位置。

retainContextWhHidden

对于界面或状态非常复杂且无法快速保存和恢复的网页视图,你可以使用retainContextWhHidden选项。该选项使网页视图保留内容但隐藏状态,即使网页视图本身不再处于前景。

虽然Cat编码几乎不能说是复杂状态,但我们试着启用retainContextWhHidden要了解该选项如何改变 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 演示版

注意现在隐藏网页视图后,计数器不会重置。不需要额外的代码!其中retainContextWhHidden,Web视图类似于网页浏览器中的背景标签页。即使标签页未激活或可见,脚本和其他动态内容仍会持续运行。你也可以在retainContextWhHidden已启用。

虽然retainContextWhHidden这可能很吸引人,但请记住,这种方法内存开销较高,只有在其他持久化技术无效时才应使用。

交通便利性

该级别VScode-使用屏幕阅读器在用户使用屏幕阅读器作 VS Code 的情境下,将被添加到你的 webview 主体中。此外,课程VScode-减少-运动当用户表达希望减少Windows中的运动量时,将添加到文档的主体元素中。通过观察这些类并相应调整渲染,你的网页浏览内容可以更好地反映用户的偏好。

下一步

如果你想了解更多VS Code的可扩展性,可以试试以下主题:

  • 扩展API——了解完整的VS Code扩展API。
  • 扩展功能——看看其他扩展 VS Code 的方法。