に参加して、VS Code の AI 支援開発について学びましょう。

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 内に存在する必要がありますか?別のアプリケーションや Web サイトとしての方が良いでしょうか?

  • Webview は機能を実装する唯一の方法ですか?代わりに通常の VS Code API を使用できますか?

  • Webview は、その高いリソース コストを正当化するのに十分なユーザー価値を追加しますか?

覚えておいてください。Webview で何かできるからといって、そうすべきだとは限りません。ただし、Webview を使用する必要があると確信している場合は、このドキュメントが役立ちます。始めましょう。

Webview API の基本

Webview API を説明するために、シンプルな拡張機能「Cat Coding」を構築します。この拡張機能は、Webview を使用して、コードを記述する猫の GIF を表示します(おそらく VS Code で)。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 バージョンを対象とする場合、activationEventsonCommand: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 を設定すると、iframe の再読み込みと同様に、Webview のコンテンツ全体が置き換えられます。これは、Webview でスクリプトを使用し始めると覚えておくべき重要なことです。なぜなら、webview.html を設定するとスクリプトの状態もリセットされるからです。

上記の例では、webview.title を使用して、エディターに表示されるドキュメントのタイトルも変更しています。タイトルを設定しても、Webview は再読み込みされません。

ライフサイクル

Webview パネルは、それを作成した拡張機能によって所有されます。拡張機能は、createWebviewPanel から返された Webview を保持する必要があります。拡張機能がこの参照を失うと、Webview が VS Code に表示され続ける場合でも、その Webview に再びアクセスすることはできません。

テキスト エディターと同様に、ユーザーはいつでも Webview パネルを閉じることができます。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
      );
    })
  );
}

拡張機能は、dispose() を呼び出すことで Webview をプログラム的に閉じることもできます。たとえば、猫の作業時間を 5 秒に制限したい場合

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 パネルは一度に 1 つのエディター列にのみ表示できます。reveal() を呼び出すか、Webview パネルを新しいエディター列にドラッグすると、Webview はその新しい列に移動します。

Webviews are moved when you drag them between tabs

一度に 1 つの 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 に変換する必要があります。

Giphy から猫の GIF を取得するのではなく、拡張機能にバンドルし始めたいと想像してください。これを行うには、まずディスク上のファイルへの 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 のセットを定義します。

localResourceRoots を使用して、Cat Coding の Webview が拡張機能の media ディレクトリからのみリソースを読み込むように制限できます

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {
          // Only allow the webview to access resources in our extension's media directory
          localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'media')]
        }
      );

      const onDiskPath = vscode.Uri.joinPath(context.extensionUri, 'media', 'cat.gif');
      const catGifSrc = panel.webview.asWebviewUri(onDiskPath);

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

すべてのローカル リソースを許可しないには、localResourceRoots[] に設定するだけです。

一般に、Webview はローカル リソースの読み込みにおいて可能な限り制限的であるべきです。ただし、localResourceRoots 自体では完全なセキュリティ保護は提供されないことに注意してください。Webview がセキュリティのベスト プラクティスにも従い、読み込めるコンテンツをさらに制限するためにコンテンツ セキュリティ ポリシーを追加してください。

Webview コンテンツのテーマ設定

Webview は CSS を使用して、VS Code の現在のテーマに基づいて外観を変更できます。VS Code はテーマを 3 つのカテゴリにグループ化し、現在のテーマを示すために 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 アプリケーションを開発する際は、3 種類のテーマすべてで機能することを確認してください。そして、視覚障害を持つ人々が使用できるように、常にハイコントラスト モードで Webview をテストしてください。

Webview は、CSS 変数を使用して VS Code のテーマ カラーにアクセスすることもできます。これらの変数名は vscode で始まり、.- に置き換えられます。たとえば、editor.foregroundvar(--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 に送信します。メッセージは、標準の message イベントを通じて Webview 内で受信されます。

これを実証するために、現在コーディング中の猫にコードをリファクタリングするように指示する (それによって総行数を減らす) 新しいコマンドを Cat Coding に追加しましょう。新しい catCoding.doRefactor コマンドは、postMessage を使用して現在の Webview に指示を送信し、Webview 自体内で window.addEventListener('message', event => { ... }) を使用してメッセージを処理します

export function activate(context: vscode.ExtensionContext) {
  // Only allow a single Cat Coder
  let currentPanel: vscode.WebviewPanel | undefined = undefined;

  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      if (currentPanel) {
        currentPanel.reveal(vscode.ViewColumn.One);
      } else {
        currentPanel = vscode.window.createWebviewPanel(
          'catCoding',
          'Cat Coding',
          vscode.ViewColumn.One,
          {
            enableScripts: true
          }
        );
        currentPanel.webview.html = getWebviewContent();
        currentPanel.onDidDispose(
          () => {
            currentPanel = undefined;
          },
          undefined,
          context.subscriptions
        );
      }
    })
  );

  // Our new command
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.doRefactor', () => {
      if (!currentPanel) {
        return;
      }

      // Send a message to our webview.
      // You can send any JSON serializable data.
      currentPanel.webview.postMessage({ command: 'refactor' });
    })
  );
}

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

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

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

        // Handle the message inside the webview
        window.addEventListener('message', event => {

            const message = event.data; // The JSON data our extension sent

            switch (message.command) {
                case 'refactor':
                    count = Math.ceil(count * 0.5);
                    counter.textContent = count;
                    break;
            }
        });
    </script>
</body>
</html>`;
}

Passing messages to a webview

Webview から拡張機能へのメッセージの受け渡し

Webview は、拡張機能にメッセージを返すこともできます。これは、Webview 内の特別な VS Code API オブジェクトの postMessage 関数を使用して行われます。VS Code API オブジェクトにアクセスするには、Webview 内で acquireVsCodeApi を呼び出します。この関数は、セッションごとに一度だけ呼び出すことができます。このメソッドによって返される VS Code API のインスタンスを保持し、それを使用する必要がある他のすべての関数に渡す必要があります。

VS Code API と postMessageCat 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 Workers の使用

Web Workers は Webview 内でサポートされていますが、いくつか重要な制限に注意する必要があります。

まず、ワーカーは data: または 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);
  });

ワーカー スクリプトは、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
    })
  ]
};

セキュリティ

通常のウェブページと同様に、Webview を作成する際には、いくつかの基本的なセキュリティ ベスト プラクティスに従う必要があります。

機能を制限する

Webview は、必要最小限の機能セットを持つべきです。たとえば、Webview がスクリプトを実行する必要がない場合、enableScripts: true を設定しないでください。Webview がユーザーのワークスペースからリソースを読み込む必要がない場合、localResourceRoots[vscode.Uri.file(extensionContext.extensionPath)] または [] に設定して、すべてのローカル リソースへのアクセスを禁止してください。

コンテンツ セキュリティ ポリシー

コンテンツ セキュリティ ポリシーは、Webview で読み込みおよび実行できるコンテンツをさらに制限します。たとえば、コンテンツ セキュリティ ポリシーは、許可されたスクリプトのリストのみが Webview で実行されるようにしたり、Webview に https 経由でのみ画像を読み込むように指示したりできます。

コンテンツ セキュリティ ポリシーを追加するには、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>`;
}

ポリシー default-src 'none'; はすべてのコンテンツを禁止します。その後、拡張機能が機能するために必要な最小限のコンテンツを再度有効にすることができます。以下は、ローカル スクリプトとスタイルシートの読み込み、および https 経由での画像の読み込みを許可するコンテンツ セキュリティ ポリシーです

<meta
  http-equiv="Content-Security-Policy"
  content="default-src 'none'; img-src ${webview.cspSource} https:; script-src ${webview.cspSource}; style-src ${webview.cspSource};"
/>

${webview.cspSource} の値は、webview オブジェクト自体から取得される値のプレースホルダーです。この値の使用方法の完全な例については、webview サンプルを参照してください。

このコンテンツ セキュリティ ポリシーは、インライン スクリプトとスタイルも暗黙的に無効にします。すべてのインライン スタイルとスクリプトを外部ファイルに抽出し、コンテンツ セキュリティ ポリシーを緩和することなく適切に読み込まれるようにすることがベスト プラクティスです。

https 経由でのみコンテンツを読み込む

Webview が外部リソースの読み込みを許可する場合、これらのリソースを https 経由でのみ許可し、http 経由では許可しないことを強くお勧めします。上記のコンテンツ セキュリティ ポリシーの例は、画像を https: 経由でのみ読み込むことを許可することで、すでにこれを行っています。

すべてのユーザー入力をサニタイズする

通常のウェブページと同様に、Webview の HTML を構築する際には、すべてのユーザー入力をサニタイズする必要があります。入力を適切にサニタイズしないと、コンテンツ インジェクションを許し、ユーザーをセキュリティ リスクにさらす可能性があります。

サニタイズする必要がある値の例

  • ファイルの内容。
  • ファイルおよびフォルダーのパス。
  • ユーザーおよびワークスペースの設定。

HTML 文字列を構築するためのヘルパー ライブラリの使用を検討するか、少なくともユーザーのワークスペースからのすべてのコンテンツが適切にサニタイズされていることを確認してください。

セキュリティのためにサニタイズだけに頼らないでください。潜在的なコンテンツ インジェクションの影響を最小限に抑えるために、コンテンツ セキュリティ ポリシーを持つなど、他のセキュリティ ベスト プラクティスに従うようにしてください。

永続化

標準の Webview のライフサイクルでは、Webview は createWebviewPanel によって作成され、ユーザーが閉じるか .dispose() が呼び出されると破棄されます。しかし、Webview の内容は、Webview が表示可能になると作成され、Webview がバックグラウンドに移動すると破棄されます。Webview がバックグラウンド タブに移動されると、Webview 内の状態は失われます。

これを解決する最善の方法は、Webview をステートレスにすることです。メッセージ パッシングを使用して Webview の状態を保存し、Webview が再び表示可能になったときに状態を復元します。

getState と setState

Webview 内で実行されるスクリプトは、getState メソッドと setState メソッドを使用して、JSON シリアル化可能な状態オブジェクトを保存および復元できます。この状態は、Webview パネルが非表示になったときに Webview コンテンツ自体が破棄された後も永続化されます。状態は、Webview パネルが破棄されると破棄されます。

// Inside a webview script
const vscode = acquireVsCodeApi();

const counter = document.getElementById('lines-of-code-counter');

// Check if we have an old state to restore from
const previousState = vscode.getState();
let count = previousState ? previousState.count : 0;
counter.textContent = count;

setInterval(() => {
  counter.textContent = count++;
  // Update the saved state
  vscode.setState({ count });
}, 100);

getStatesetState は、retainContextWhenHidden よりもパフォーマンス オーバーヘッドがはるかに低いため、状態を永続化するための推奨される方法です。

シリアル化

WebviewPanelSerializer を実装することで、VS Code が再起動したときに Webview を自動的に復元できます。シリアル化は getStatesetState を基にしており、拡張機能が Webview 用に WebviewPanelSerializer を登録した場合にのみ有効になります。

コーディング中の猫を VS Code の再起動時に永続化させるには、まず拡張機能の package.jsononWebviewPanel アクティベーション イベントを追加します

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

このアクティベーション イベントにより、VS Code が catCoding ビュータイプの Webview を復元する必要があるたびに、拡張機能がアクティブ化されることが保証されます。

次に、拡張機能の activate メソッドで、registerWebviewPanelSerializer を呼び出して新しい WebviewPanelSerializer を登録します。WebviewPanelSerializer は、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 は Web ブラウザのバックグラウンド タブと同様に機能します。スクリプトやその他の動的なコンテンツは、タブがアクティブまたは表示されていなくても実行され続けます。retainContextWhenHidden が有効になっている場合、非表示の Webview にメッセージを送信することもできます。

retainContextWhenHidden は魅力的かもしれませんが、メモリオーバーヘッドが大きいことに注意し、他の永続化技術が機能しない場合にのみ使用すべきです。

アクセシビリティ

ユーザーがスクリーン リーダーを使用して VS Code を操作しているコンテキストでは、vscode-using-screen-reader クラスが Webview のメイン body に追加されます。さらに、ユーザーがウィンドウ内のモーション量を減らすことを好むことを表明している場合は、vscode-reduce-motion クラスがドキュメントのメイン body 要素に追加されます。これらのクラスを観察し、それに応じてレンダリングを調整することで、Webview のコンテンツはユーザーの設定をよりよく反映できます。

次のステップ

VS Code の拡張性についてさらに詳しく知りたい場合は、次のトピックを試してください

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