🚀 在 VS Code 中取得

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,那麼本文檔將在此處提供協助。讓我們開始吧。

Webview 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 建立後更新其內容。讓我們使用它來讓 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
      );
    })
  );
}

擴充功能也可以透過呼叫 webview 上的 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 以允許存取其他本機資源。

您也可以始終使用資料 URI 將資源直接嵌入到 webview 中。

控制對本機資源的存取

Webview 可以使用 localResourceRoots 選項控制可以從使用者電腦載入哪些資源。localResourceRoots 定義一組根 URI,可以從這些根 URI 載入本機內容。

我們可以使用 localResourceRootsCat 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,請先在新的 webview/context 區段下的 menus 中新增一個新項目。每個貢獻都採用 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。訊息會在 webview 內透過標準 message 事件接收。

為了示範這一點,讓我們為 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 載入影像。

若要新增內容安全策略,請將 <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 允許載入外部資源,強烈建議您僅允許透過 https 而非 http 載入這些資源。上面的範例內容安全策略已經透過僅允許透過 https: 載入影像來執行此操作。

清理所有使用者輸入

就像對待一般網頁一樣,在建構 webview 的 HTML 時,您必須清理所有使用者輸入。未能正確清理輸入可能會允許內容注入,這可能會讓您的使用者面臨安全風險。

必須清理的範例值

  • 檔案內容。
  • 檔案和資料夾路徑。
  • 使用者和工作區設定。

考慮使用輔助程式庫來建構您的 HTML 字串,或至少確保使用者工作區中的所有內容都已正確清理。

永遠不要僅僅依靠清理來確保安全。請務必遵循其他安全最佳實務,例如具有內容安全策略,以最大程度地減少任何潛在內容注入的影響。

持久性

在標準 webview 生命週期中,webview 由 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,您的 webview 可以在 VS Code 重新啟動時自動還原。序列化建立在 getStatesetState 的基礎之上,並且僅在您的擴充功能為您的 webview 註冊 WebviewPanelSerializer 時啟用。

若要讓我們的編碼貓咪在 VS Code 重新啟動後仍然存在,請先將 onWebviewPanel 啟用事件新增至擴充功能的 package.json

"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

對於具有非常複雜的 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 的行為類似於網頁瀏覽器中的背景索引標籤。即使索引標籤未處於使用中或可見狀態,指令碼和其他動態內容也會繼續執行。啟用 retainContextWhenHidden 後,您也可以將訊息傳送至隱藏的 webview。

雖然 retainContextWhenHidden 可能很吸引人,但請記住,這具有很高的記憶體負擔,並且僅應在其他持久性技術無法運作時使用。

協助工具

在使用者使用螢幕閱讀器操作 VS Code 的情況下,類別 vscode-using-screen-reader 將會新增至您的 webview 的主要 body 中。此外,在使用者表示偏好減少視窗中的動態效果的情況下,類別 vscode-reduce-motion 將會新增至文件的主要 body 元素中。透過觀察這些類別並相應地調整您的呈現方式,您的 webview 內容可以更好地反映使用者的偏好。

後續步驟

如果您想進一步了解 VS Code 擴充性,請嘗試以下主題