VS Codeのエージェントモードを拡張するには、を試してください!

Webview API

Webview APIを使用すると、拡張機能はVisual Studio Code内で完全にカスタマイズ可能なビューを作成できます。例えば、組み込みのMarkdown拡張機能はwebviewsを使用してMarkdownプレビューをレンダリングします。Webviewsは、VS CodeのネイティブAPIがサポートする範囲を超えた複雑なユーザーインターフェースを構築するためにも使用できます。

webviewは、拡張機能が制御するVS Code内のiframeと考えてください。webviewはこのフレーム内でほぼすべてのHTMLコンテンツをレンダリングでき、メッセージパッシングを使用して拡張機能と通信します。この自由度により、webviewsは信じられないほど強力になり、拡張機能の可能性をまったく新しい範囲に広げます。

WebviewはいくつかのVS Code APIで使用されています

  • createWebviewPanelを使用して作成されたWebviewパネル。この場合、WebviewパネルはVS Codeで個別のエディターとして表示されます。これにより、カスタムUIやカスタムビジュアライゼーションの表示に役立ちます。
  • カスタムエディターのビューとして。カスタムエディターを使用すると、拡張機能はワークスペース内の任意のファイルを編集するためのカスタムUIを提供できます。カスタムエディターAPIは、元に戻すややり直しなどのエディターイベント、および保存などのファイルイベントに拡張機能がフックすることも可能にします。
  • サイドバーまたはパネル領域にレンダリングされるWebviewビューとして。詳細については、webviewビューサンプル拡張機能を参照してください。

このページでは基本的なwebviewパネルAPIに焦点を当てていますが、ここで説明するほとんどすべてはカスタムエディターやwebviewビューで使用されるwebviewsにも適用されます。これらの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"
  }
}

: 拡張機能がVS Codeのバージョン1.74より前を対象としている場合、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の検査とデバッグ

Developer: Toggle Developer Toolsコマンドを実行すると、Developer Toolsウィンドウが開き、webviewsのデバッグと検査に使用できます。

The developer tools

VS Codeのバージョン1.56より古いものを使用している場合、またはenableFindWidgetを設定するwebviewをデバッグしようとしている場合は、代わりにDeveloper: Open Webview Developer Toolsコマンドを使用する必要があることに注意してください。このコマンドは、すべてのwebviewsとエディター自体で共有されるDeveloper Toolsページを使用するのではなく、各webview専用のDeveloper Toolsページを開きます。

Developer Toolsから、Developer Toolsウィンドウの左上隅にある検査ツールを使用してwebviewのコンテンツを検査し始めることができます。

Inspecting a webview using the developer tools

また、開発者ツールのコンソールでwebviewからのすべてのエラーとログを表示することもできます。

The developer tools console

webviewのコンテキストで式を評価するには、開発者ツールのコンソールパネルの左上隅にあるドロップダウンからアクティブフレーム環境を選択していることを確認してください。

Selecting the active frame

アクティブフレーム環境は、webviewスクリプト自体が実行される場所です。

さらに、Developer: Reload Webviewコマンドは、すべてのアクティブなwebviewsを再ロードします。これは、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をロードします!

デフォルトでは、webviewsは以下の場所にあるリソースにのみアクセスできます。

  • 拡張機能のインストールディレクトリ内。
  • ユーザーの現在アクティブなワークスペース内。

追加のローカルリソースへのアクセスを許可するには、WebviewOptions.localResourceRootsを使用します。

また、データURIを使用してリソースをwebviewに直接埋め込むことも常に可能です。

ローカルリソースへのアクセスを制御する

Webviewsは、localResourceRootsオプションを使用して、ユーザーのマシンからロードできるリソースを制御できます。localResourceRootsは、ローカルコンテンツがロードされる可能性のあるルートURIのセットを定義します。

localResourceRootsを使用して、Cat Coding webviewsが拡張機能内の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[]に設定するだけです。

一般的に、webviewsはローカルリソースのロードにおいて可能な限り制限的であるべきです。ただし、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を記述する必要がある特殊なケースでは、webviewsのbody要素には、現在アクティブなテーマのIDを格納するvscode-theme-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を使用する必要があります。

コンテキストメニュー

高度なwebviewsは、ユーザーがwebview内で右クリックしたときに表示されるコンテキストメニューをカスタマイズできます。これは、VS Codeの通常のコンテキストメニューと同様に寄与点を使用して行われるため、カスタムメニューはエディターの他の部分とうまく調和します。Webviewは、webviewの異なるセクションに対してカスタムコンテキストメニューを表示することもできます。

webviewに新しいコンテキストメニュー項目を追加するには、まず新しいwebview/contextセクションの下のmenusに新しいエントリを追加します。各寄与にはcommand(項目名もここから来ます)とwhen句が必要です。when句には、コンテキストメニューが拡張機能のwebviewsにのみ適用されるように、webviewId == 'YOUR_WEBVIEW_VIEW_TYPE'を含める必要があります。

"contributes": {
  "menus": {
    "webview/context": [
      {
        "command": "catCoding.yarn",
        "when": "webviewId == 'catCoding'"
      },
      {
        "command": "catCoding.insertLion",
        "when": "webviewId == 'catCoding' && webviewSection == 'editor'"
      }
    ]
  },
  "commands": [
    {
      "command": "catCoding.yarn",
      "title": "Yarn 🧶",
      "category": "Cat Coding"
    },
    {
      "command": "catCoding.insertLion",
      "title": "Insert 🦁",
      "category": "Cat Coding"
    },
    ...
  ]
}

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はデフォルトでwebviewsでは無効になっていますが、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スクリプトは、通常のウェブページのスクリプトができることとほぼ同じことができます。ただし、webviewsは独自のコンテキストで存在するため、webview内のスクリプトはVS Code APIにアクセスできないことに注意してください。そこでメッセージパッシングが登場します!

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

拡張機能は、webview.postMessage()を使用してwebviewsにデータを送信できます。このメソッドは、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を呼び出します。この関数はセッションごとに1回しか呼び出すことができません。このメソッドから返される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 Workersの使用

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

まず、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を設定しないでください。ユーザーのワークスペースからリソースをロードする必要がない場合は、すべてのローカルリソースへのアクセスを禁止するためにlocalResourceRoots[vscode.Uri.file(extensionContext.extensionPath)]または[]に設定してください。

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

コンテンツセキュリティポリシーは、webviewsでロードおよび実行できるコンテンツをさらに制限します。例えば、コンテンツセキュリティポリシーは、許可されたスクリプトのリストのみが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が外部リソースのロードを許可する場合、これらのリソースをhttp経由ではなくhttps経由でのみロードすることを強くお勧めします。上記のコンテンツセキュリティポリシーの例では、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が再起動したときにwebviewsが自動的に復元されるようになります。シリアル化はgetStatesetStateに基づいており、拡張機能がwebviewsのWebviewPanelSerializerを登録した場合にのみ有効になります。

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

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

このアクティベーションイベントは、VS CodeがviewType: catCodingのwebviewを復元する必要があるたびに、私たちの拡張機能がアクティブ化されることを保証します。

次に、拡張機能のactivateメソッドで、registerWebviewPanelSerializerを呼び出して新しいWebviewPanelSerializerを登録します。WebviewPanelSerializerは、webviewのコンテンツをその永続化された状態から復元する責任を負います。この状態は、webviewコンテンツがsetStateを使用して設定したJSONブロブです。

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や、迅速に保存・復元できない状態を持つwebviewsの場合、代わりにretainContextWhenHiddenオプションを使用できます。このオプションは、webview自体がフォアグラウンドにない場合でも、webviewがそのコンテンツを保持し、非表示の状態を維持するようにします。

Cat Codingは複雑な状態を持つとは言い難いですが、このオプションがwebviewの動作をどのように変えるかを見るために、retainContextWhenHiddenを有効にしてみましょう。

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のメインボディに追加されます。さらに、ユーザーがウィンドウ内のモーション量を減らすことを希望している場合、ドキュメントのメインボディ要素にvscode-reduce-motionクラスが追加されます。これらのクラスを監視し、それに応じてレンダリングを調整することで、webviewコンテンツはユーザーの好みをより適切に反映できます。

次のステップ

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