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 和自訂視覺化內容。
  • 作為 自訂編輯器 (custom editor) 的檢視。自訂編輯器允許擴充功能提供自訂 UI 來編輯工作區中的任何檔案。自訂編輯器 API 還允許您的擴充功能連結至編輯器事件(如復原與重做)以及檔案事件(如儲存)。
  • 在側邊欄或面板區域中呈現的 Webview 檢視 (Webview views)。詳情請參閱 Webview 檢視範例擴充功能

本頁重點介紹基本的 Webview 面板 API,儘管此處涵蓋的內容幾乎全都適用於自訂編輯器和 Webview 檢視中的 Webview。即使您對後者更感興趣,我們仍建議您先通讀本頁,以熟悉 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,裡面有我們的貓。使用者可以從命令選擇區 (Command Palette)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,即使 Webview 仍會在 VS Code 中繼續顯示。

與文字編輯器一樣,使用者隨時可以關閉 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() 來以程式設計方式關閉它們。例如,如果我們想將貓咪的工作時間限制為五秒

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

Developer: Toggle Developer Tools 命令會開啟一個 開發人員工具 (Developer Tools) 視窗,您可以用它來偵錯和檢查您的 Webview。

The developer tools

請注意,如果您使用的是 1.56 之前的 VS Code 版本,或者您正在偵錯設定了 enableFindWidget 的 Webview,則必須改用 Developer: Open Webview Developer Tools 命令。此命令會為每個 Webview 開啟一個專用的開發人員工具頁面,而不是使用由所有 Webview 與編輯器本身共用的開發人員工具頁面。

在開發人員工具中,您可以使用開發人員工具視窗左上角的檢查工具,開始檢查 Webview 的內容

Inspecting a webview using the developer tools

您也可以在開發人員工具主控台中檢視 Webview 的所有錯誤與記錄

The developer tools console

若要在 Webview 的環境中評估表達式,請確保從開發人員工具主控台面板左上角的下拉式選單中選擇作用中框架 (active frame) 環境

Selecting the active frame

作用中框架環境就是 Webview 指令碼實際執行的地方。

此外,Developer: Reload Webview 命令會重新載入所有作用中的 Webview。如果您需要重設 Webview 的狀態,或者磁碟上的某些 Webview 內容已變更且您希望載入新內容,這會很有幫助。

載入本機內容

Webview 在隔離的環境中執行,無法直接存取本機資源。這是基於安全性考量。這意味著,為了從您的擴充功能載入圖片、樣式表和其他資源,或從使用者目前的工作區載入任何內容,您必須使用 Webview.asWebviewUri 函式,將本機的 file: URI 轉換為 VS Code 可用於載入部分本機資源的特殊 URI。

假設我們想要開始將貓咪的 GIF 打包到我們的擴充功能中,而不是從 Giphy 拉取。為此,我們首先為磁碟上的檔案建立一個 URI,然後透過 asWebviewUri 函式傳遞這些 URI

import * as vscode from 'vscode';

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

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

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

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

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

如果我們偵錯這段程式碼,我們會看到 catGifSrc 的實際值類似於

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

VS Code 能夠理解這個特殊 URI,並將使用它從磁碟載入我們的 GIF!

預設情況下,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 也遵循安全最佳實踐,並新增內容安全性原則 (Content Security Policy) 以進一步限制可載入的內容。

佈景主題化 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

內容選單 (Context menus)

進階 Webview 可以自訂當使用者在 Webview 內按右鍵時顯示的內容選單。這是使用貢獻點 (contribution point) 來完成的,類似於 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 一樣,這意味著它們也可以執行指令碼。JavaScript 在 Webview 中預設為停用,但可以透過傳入 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 實例,並將其傳遞給任何需要使用它的其他函式。

我們可以在 Cat Coding Webview 中使用 VS Code API 和 postMessage,在貓咪引入錯誤時通知擴充功能

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

      panel.webview.html = getWebviewContent();

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

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

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

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

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

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)] 甚至 [],以禁止存取所有本機資源。

內容安全性原則

內容安全性原則 (Content Security Policy) 會進一步限制 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 內執行的指令碼可以使用 getStatesetState 方法來儲存並還原 JSON 可序列化的狀態物件。即使 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 重新啟動後持續存在,首先要在擴充功能的 package.json 中新增 onWebviewPanel 啟用事件

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

此啟用事件確保每當 VS Code 需要還原 viewType 為 catCoding 的 Webview 時,我們的擴充功能都會被啟用。

然後,在我們擴充功能的 activate 方法中,呼叫 registerWebviewPanelSerializer 來註冊一個新的 WebviewPanelSerializerWebviewPanelSerializer 負責從其持久化狀態還原 Webview 的內容。此狀態即為 Webview 內容使用 setState 所設定的 JSON blob。

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

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

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

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

現在,如果您在開啟 Cat Coding 面板的情況下重新啟動 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 可能很吸引人,但請記住它的記憶體開銷很高,只有在其他持久化技術無法運作時才應使用。

協助工具

類別 vscode-using-screen-reader 將會在使用使用者透過螢幕閱讀器操作 VS Code 的環境中,加入到您 Webview 的主要 body 元素。此外,如果使用者表示偏好減少視窗中的動態效果,類別 vscode-reduce-motion 也會加入到文件的主要 body 元素中。透過觀察這些類別並據此調整您的呈現方式,您的 Webview 內容將能更符合使用者的偏好。

後續步驟

如果您想進一步了解 VS Code 的擴充能力,請嘗試這些主題

© . This site is unofficial and not affiliated with Microsoft.