🚀 VS Code で で入手しましょう!

ウェブビュー API

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

ウェブビューを、拡張機能が制御する VS Code 内の iframe と考えてください。ウェブビューはこのフレーム内でほぼすべての HTML コンテンツをレンダリングでき、メッセージパッシングを使用して拡張機能と通信します。この自由度により、ウェブビューは非常に強力になり、拡張機能の可能性が大きく広がります。

ウェブビューは、いくつかの VS Code API で使用されています

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

このページでは、基本的なウェブビューパネル API に焦点を当てていますが、ここで説明するほぼすべてのことは、カスタムエディターやウェブビュービューで使用されるウェブビューにも当てはまります。これらの API にもっと興味がある場合でも、最初にこのページを読んで、ウェブビューの基本について理解することを推奨します。

VS Code API の使用

ウェブビューを使うべきか?

ウェブビューは非常に優れていますが、VS Code のネイティブ API が不十分な場合にのみ、控えめに使用する必要があります。ウェブビューはリソースを大量に消費し、通常の拡張機能とは別のコンテキストで実行されます。設計が不十分なウェブビューは、VS Code 内で場違いに感じられる可能性もあります。

ウェブビューを使用する前に、以下を検討してください

  • この機能は本当に VS Code 内に存在する必要がありますか?別のアプリケーションまたは Web サイトとしての方が適切ではありませんか?

  • ウェブビューは、機能を実装する唯一の方法ですか?代わりに通常の VS Code API を使用できますか?

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

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

ウェブビュー API の基本

ウェブビュー API について説明するために、Cat Coding という簡単な拡張機能を構築します。この拡張機能は、ウェブビューを使用して、コード (おそらく VS Code で) を書いている猫の GIF アニメーションを表示します。API について説明する中で、猫が書いたソースコードの行数を追跡するカウンターや、猫がバグを導入したときにユーザーに通知する通知など、機能を拡張機能に追加していきます。

これは、Cat Coding 拡張機能の最初のバージョンの package.json です。サンプルアプリの完全なコードはこちらにあります。拡張機能の最初のバージョンでは、catCoding.start というコマンドをコントリビューションします。ユーザーがこのコマンドを呼び出すと、猫が表示されたシンプルなウェブビューが表示されます。ユーザーは、コマンドパレットから 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 コマンドを登録し、それを使用して基本的なウェブビューを表示します。

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      // Create and show a new webview
      const panel = vscode.window.createWebviewPanel(
        'catCoding', // Identifies the type of the webview. Used internally
        'Cat Coding', // Title of the panel displayed to the user
        vscode.ViewColumn.One, // Editor column to show the new webview panel in.
        {} // Webview options. More on these later.
      );
    })
  );
}

vscode.window.createWebviewPanel 関数は、エディターでウェブビューを作成して表示します。現在の状態で catCoding.start コマンドを実行しようとすると、次のようになります。

An empty webview

コマンドは正しいタイトルの新しいウェブビューパネルを開きますが、コンテンツはありません!新しいパネルに猫を追加するには、webview.html を使用してウェブビューの 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>`;
}

コマンドを再度実行すると、ウェブビューは次のようになります。

A webview with some HTML

進捗状況!

webview.html は常に完全な HTML ドキュメントである必要があります。HTML フラグメントまたは不正な形式の HTML は、予期しない動作を引き起こす可能性があります。

ウェブビューコンテンツの更新

webview.html は、作成後にウェブビューのコンテンツを更新することもできます。これを使用して、猫のローテーションを導入することで、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.html を設定すると、スクリプトの状態もリセットされるためです。

上記の例では、webview.title を使用して、エディターに表示されるドキュメントのタイトルを変更しています。タイトルを設定しても、ウェブビューがリロードされることはありません。

ライフサイクル

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

テキストエディターと同様に、ユーザーはいつでもウェブビューパネルを閉じることができます。ユーザーによってウェブビューパネルが閉じられると、ウェブビュー自体が破棄されます。破棄されたウェブビューを使用しようとすると、例外がスローされます。これは、setInterval を使用する上記の例に実際には重要なバグがあることを意味します。ユーザーがパネルを閉じると、setInterval が引き続き発生し、panel.webview.html を更新しようとしますが、もちろん例外がスローされます。猫は例外を嫌います。これを修正しましょう!

onDidDispose イベントは、ウェブビューが破棄されるときに発生します。このイベントを使用して、それ以上の更新をキャンセルし、ウェブビューのリソースをクリーンアップできます。

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() を呼び出すことによって、プログラムでウェブビューを閉じることもできます。たとえば、猫の労働時間を 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
      );
    })
  );
}

可視性と移動

ウェブビューパネルがバックグラウンドタブに移動されると、非表示になります。ただし、破棄されるわけではありません。VS Code は、パネルが再びフォアグラウンドに表示されると、webview.html からウェブビューのコンテンツを自動的に復元します。

Webview content is automatically restored when the webview becomes visible again

.visible プロパティは、ウェブビューパネルが現在表示されているかどうかを示します。

拡張機能は、reveal() を呼び出すことによって、ウェブビューパネルをプログラムでフォアグラウンドに表示できます。このメソッドは、パネルを表示するオプションのターゲットビュー列を取得します。ウェブビューパネルは、一度に 1 つのエディター列にのみ表示できます。reveal() を呼び出すか、ウェブビューパネルを新しいエディター列にドラッグすると、ウェブビューがその新しい列に移動します。

Webviews are moved when you drag them between tabs

拡張機能を更新して、一度に 1 つのウェブビューのみが存在できるようにしましょう。パネルがバックグラウンドにある場合、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

ウェブビューの可視性が変更されるたび、またはウェブビューが新しい列に移動されると、onDidChangeViewState イベントが発生します。拡張機能はこのイベントを使用して、ウェブビューが表示されている列に基づいて猫を変更できます。

const cats = {
  'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
  'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif',
  'Testing Cat': 'https://media.giphy.com/media/3oriO0OEd9QIDdllqo/giphy.gif'
};

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

      // Update contents based on view state changes
      panel.onDidChangeViewState(
        e => {
          const panel = e.webviewPanel;
          switch (panel.viewColumn) {
            case vscode.ViewColumn.One:
              updateWebviewForCat(panel, 'Coding Cat');
              return;

            case vscode.ViewColumn.Two:
              updateWebviewForCat(panel, 'Compiling Cat');
              return;

            case vscode.ViewColumn.Three:
              updateWebviewForCat(panel, 'Testing Cat');
              return;
          }
        },
        null,
        context.subscriptions
      );
    })
  );
}

function updateWebviewForCat(panel: vscode.WebviewPanel, catName: keyof typeof cats) {
  panel.title = catName;
  panel.webview.html = getWebviewContent(catName);
}

Responding to onDidChangeViewState events

ウェブビューの検査とデバッグ

開発者: 開発者ツールを切り替え コマンドは、ウェブビューのデバッグと検査に使用できる開発者ツールウィンドウを開きます。

The developer tools

VS Code のバージョンが 1.56 より古い場合、または enableFindWidget を設定するウェブビューをデバッグしようとしている場合は、代わりに 開発者: ウェブビュー開発者ツールを開く コマンドを使用する必要があります。このコマンドは、すべてのウェブビューとエディター自体で共有される開発者ツールページを使用する代わりに、各ウェブビュー専用の開発者ツールページを開きます。

開発者ツールから、開発者ツールウィンドウの左上隅にある検査ツールを使用して、ウェブビューのコンテンツの検査を開始できます。

Inspecting a webview using the developer tools

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

The developer tools console

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

Selecting the active frame

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

さらに、開発者: ウェブビューをリロード コマンドは、すべてのアクティブなウェブビューをリロードします。これは、ウェブビューの状態をリセットする必要がある場合や、ディスク上のウェブビューコンテンツが変更され、新しいコンテンツをロードしたい場合に役立ちます。

ローカルコンテンツの読み込み

ウェブビューは、ローカルリソースに直接アクセスできない分離されたコンテキストで実行されます。これはセキュリティ上の理由で行われます。つまり、拡張機能から画像、スタイルシート、その他のリソースをロードしたり、ユーザーの現在のワークスペースからコンテンツをロードしたりするには、Webview.asWebviewUri 関数を使用して、ローカルの file: URI を、VS Code がローカルリソースのサブセットをロードするために使用できる特別な URI に変換する必要があります。

猫の GIF アニメーションを Giphy からプルするのではなく、拡張機能にバンドルしたいと想像してください。これを行うには、最初にディスク上のファイルへの URI を作成し、次にこれらの URI を asWebviewUri 関数に渡します。

import * as vscode from 'vscode';

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

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

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

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

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

このコードをデバッグすると、catGifSrc の実際の値は次のようになります。

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

VS Code はこの特別な URI を理解し、それを使用してディスクから GIF アニメーションをロードします!

デフォルトでは、ウェブビューは次の場所のリソースにのみアクセスできます

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

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

データ URI を使用して、リソースをウェブビューに直接埋め込むことも常に可能です。

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

ウェブビューは、localResourceRoots オプションを使用して、ユーザーのマシンからロードできるリソースを制御できます。localResourceRoots は、ローカルコンテンツをロードできるルート URI のセットを定義します。

localResourceRoots を使用して、Cat Coding ウェブビューを拡張機能の 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[] に設定するだけです。

一般に、ウェブビューはローカルリソースのロードを可能な限り制限する必要があります。ただし、localResourceRoots はそれ自体では完全なセキュリティ保護を提供しないことに注意してください。ウェブビューがセキュリティのベストプラクティスにも従っていることを確認し、ロードできるコンテンツをさらに制限するためにコンテンツセキュリティポリシーを追加してください。

ウェブビューコンテンツのテーマ設定

ウェブビューは CSS を使用して、VS Code の現在のテーマに基づいて外観を変更できます。VS Code はテーマを 3 つのカテゴリにグループ化し、現在のテーマを示す特別なクラスを body 要素に追加します。

  • vscode-light - ライトテーマ。
  • vscode-dark - ダークテーマ。
  • vscode-high-contrast - ハイコントラストテーマ。

次の CSS は、ユーザーの現在のテーマに基づいてウェブビューのテキストの色を変更します。

body.vscode-light {
  color: black;
}

body.vscode-dark {
  color: white;
}

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

ウェブビューアプリケーションを開発するときは、3 種類のテーマで動作することを確認してください。また、ハイコントラストモードでウェブビューを常にテストして、視覚障碍のある人が使用できるようにしてください。

ウェブビューは、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 を記述する必要がある特殊なケースでは、ウェブビューの body 要素に、現在アクティブなテーマの ID を格納する vscode-theme-id というデータ属性があります。これにより、ウェブビューのテーマ固有の CSS を記述できます。

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

サポートされているメディア形式

ウェブビューはオーディオとビデオをサポートしていますが、すべてのメディアコーデックまたはメディアファイルコンテナタイプがサポートされているわけではありません。

ウェブビューで使用できるオーディオ形式は次のとおりです。

  • Wav
  • Mp3
  • Ogg
  • Flac

ウェブビューで使用できるビデオ形式は次のとおりです。

  • H.264
  • VP8

ビデオファイルの場合、ビデオとオーディオトラックのメディア形式の両方がサポートされていることを確認してください。たとえば、多くの .mp4 ファイルはビデオに H.264 を使用し、オーディオに AAC を使用しています。VS Code は mp4 のビデオ部分を再生できますが、AAC オーディオはサポートされていないため、サウンドは再生されません。代わりに、オーディオトラックに mp3 を使用する必要があります。

コンテキストメニュー

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

新しいコンテキストメニュー項目をウェブビューに追加するには、最初に新しい webview/context セクションの下の menus に新しいエントリを追加します。各コントリビューションは、command (項目のタイトルもここから取得されます) と when 句を受け取ります。when 句には、コンテキストメニューが拡張機能のウェブビューにのみ適用されるように、webviewId == 'YOUR_WEBVIEW_VIEW_TYPE' を含める必要があります。

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

ウェブビュー内では、data-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 が通常ウェブビューコンテキストメニューに追加するコピーと貼り付けのエントリを非表示にする特別なコンテキストです。

ユーザーが <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

スクリプトとメッセージパッシング

ウェブビューは iframe と同じであるため、スクリプトを実行することもできます。JavaScript はデフォルトでウェブビューで無効になっていますが、enableScripts: true オプションを渡すことで簡単に再度有効にできます。

スクリプトを使用して、猫が書いたソースコードの行数を追跡するカウンターを追加してみましょう。基本的なスクリプトを実行するのは非常に簡単ですが、この例はデモンストレーションのみを目的としていることに注意してください。実際には、ウェブビューは常にコンテンツセキュリティポリシーを使用してインラインスクリプトを無効にする必要があります。

import * as path from 'path';
import * as vscode from 'vscode';

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

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

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

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

        let count = 0;
        setInterval(() => {
            counter.textContent = count++;
        }, 100);
    </script>
</body>
</html>`;
}

A script running in a webview

すごい!それは生産性の高い猫です。

ウェブビュースクリプトは、通常の Web ページ上のスクリプトができるほとんどすべてのことができます。ただし、ウェブビューは独自のコンテキストに存在するため、ウェブビュースクリプトは VS Code API にアクセスできないことに注意してください。そこでメッセージパッシングが登場します。

拡張機能からウェブビューへのメッセージの受け渡し

拡張機能は、webview.postMessage() を使用してデータをウェブビューに送信できます。このメソッドは、JSON シリアライズ可能なデータをウェブビューに送信します。メッセージは、標準の message イベントを介してウェブビュー内で受信されます。

これをデモンストレーションするために、現在コーディング中の猫にコードをリファクタリングするように指示する (これにより、合計行数が減少します) 新しいコマンドを Cat Coding に追加しましょう。新しい catCoding.doRefactor コマンドは、postMessage を使用して命令を現在のウェブビューに送信し、ウェブビュー自体の内部で 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

ウェブビューから拡張機能へのメッセージの受け渡し

ウェブビューは、メッセージを拡張機能に送り返すこともできます。これは、ウェブビュー内の特別な VS Code API オブジェクトの postMessage 関数を使用して実現されます。VS Code API オブジェクトにアクセスするには、ウェブビュー内で acquireVsCodeApi を呼び出します。この関数は、セッションごとに 1 回のみ呼び出すことができます。このメソッドから返された VS Code API のインスタンスを保持し、それを使用する必要がある他の関数に渡す必要があります。

VS Code API と postMessageCat Coding ウェブビューで使用して、猫がコードにバグを導入したときに拡張機能に警告することができます。

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 はウェブビュー内でサポートされていますが、注意すべき重要な制限がいくつかあります。

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

セキュリティ

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

機能の制限

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

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

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

コンテンツセキュリティポリシーを追加するには、ウェブビューの <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} 値は、ウェブビューオブジェクト自体から取得される値のプレースホルダーです。この値の使用方法の詳細については、ウェブビューサンプルを参照してください。

このコンテンツセキュリティポリシーは、インラインスクリプトとスタイルも暗黙的に無効にします。すべてのインラインスタイルとスクリプトを外部ファイルに抽出して、コンテンツセキュリティポリシーを緩和せずに適切にロードできるようにすることをお勧めします。

https 経由でのみコンテンツをロードする

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

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

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

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

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

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

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

永続性

標準のウェブビューライフサイクルでは、ウェブビューは createWebviewPanel によって作成され、ユーザーが閉じるとき、または .dispose() が呼び出されたときに破棄されます。ただし、ウェブビューのコンテンツは、ウェブビューが表示されるときに作成され、ウェブビューがバックグラウンドに移動されるときに破棄されます。ウェブビュー内の状態は、ウェブビューがバックグラウンドタブに移動されると失われます。

これを解決する最良の方法は、ウェブビューをステートレスにすることです。メッセージパッシングを使用してウェブビューの状態を保存し、ウェブビューが再び表示されたときに状態を復元します。

getState と setState

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

// 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 を再起動したときにウェブビューを自動的に復元できます。シリアライズは getState および setState を基盤としており、拡張機能がウェブビューの WebviewPanelSerializer を登録した場合にのみ有効になります。

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

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

このアクティベーションイベントは、VS Code が viewType: catCoding でウェブビューを復元する必要がある場合に常に拡張機能がアクティブ化されるようにします。

次に、拡張機能の activate メソッドで、registerWebviewPanelSerializer を呼び出して、新しい WebviewPanelSerializer を登録します。WebviewPanelSerializer は、永続化された状態からウェブビューのコンテンツを復元する役割を担います。この状態は、ウェブビューコンテンツが 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 または状態を持つウェブビューで、すばやく保存および復元できない場合は、代わりに retainContextWhenHidden オプションを使用できます。このオプションを使用すると、ウェブビュー自体がフォアグラウンドにない場合でも、ウェブビューはコンテンツを非表示の状態で保持します。

Cat Coding は複雑な状態を持っているとは言えませんが、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

ウェブビューが非表示にされてから復元されても、カウンターがリセットされなくなったことに注目してください。追加のコードは必要ありません!retainContextWhenHidden を使用すると、ウェブビューは Web ブラウザーのバックグラウンドタブと同様に動作します。スクリプトやその他の動的コンテンツは、タブがアクティブまたは表示されていない場合でも実行され続けます。retainContextWhenHidden が有効になっている場合は、非表示のウェブビューにメッセージを送信することもできます。

retainContextWhenHidden は魅力的かもしれませんが、これはメモリオーバーヘッドが高く、他の永続化手法が機能しない場合にのみ使用する必要があることに注意してください。

アクセシビリティ

ユーザーがスクリーンリーダーで VS Code を操作しているコンテキストでは、クラス vscode-using-screen-reader がウェブビューのメイン body に追加されます。さらに、ユーザーがウィンドウ内のモーション量を減らすことを希望している場合、クラス vscode-reduce-motion がドキュメントのメイン body 要素に追加されます。これらのクラスを監視し、それに応じてレンダリングを調整することで、ウェブビューコンテンツはユーザーの好みをより適切に反映できます。

次のステップ

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

  • 拡張機能 API - 完全な VS Code 拡張機能 API について学びます。
  • 拡張機能 - VS Code を拡張する他の方法を見てください。