Webview API
Webview API を使用すると、拡張機能は Visual Studio Code 内で完全にカスタマイズ可能なビューを作成できます。例えば、組み込みの Markdown 拡張機能は、Markdown プレビューをレンダリングするために Webview を使用しています。Webview は、VS Code のネイティブ API でサポートされている範囲を超えた複雑なユーザーインターフェースを構築するためにも使用できます。
Webview とは、拡張機能が制御する VS Code 内の iframe のようなものだと考えてください。Webview はこのフレーム内にほぼすべての HTML コンテンツをレンダリングでき、メッセージパッシングを使用して拡張機能と通信します。この自由度により Webview は非常に強力になり、拡張機能の可能性が大きく広がります。
Webview はいくつかの VS Code API で使用されています
createWebviewPanelを使用して作成される Webview パネル。この場合、Webview パネルは VS Code 内で独立したエディターとして表示されます。これは、カスタム UI や独自の視覚化を表示するのに便利です。- カスタムエディターのビューとして。カスタムエディターを使用すると、ワークスペース内のあらゆるファイルを編集するためのカスタム UI を拡張機能から提供できます。また、カスタムエディター API を使用すると、元に戻す(undo)ややり直し(redo)などのエディターイベントや、保存などのファイルイベントに拡張機能をフックさせることも可能です。
- サイドバーやパネルエリアにレンダリングされる Webview ビュー。詳細については、Webview ビューのサンプル拡張機能を参照してください。
このページでは基本的な Webview パネル API に焦点を当てていますが、ここで説明する内容は、カスタムエディターや Webview ビューで使用される Webview にもほぼすべて適用されます。これらの API に興味がある場合でも、まずこのページを通して Webview の基本を理解することをお勧めします。
リンク
VS Code API の使用法
Webview を使うべきか?
Webview は非常に素晴らしいものですが、使用は控えめにし、VS Code のネイティブ API では不十分な場合にのみ使用すべきです。Webview はリソースを大量に消費し、通常の拡張機能とは別のコンテキストで実行されます。設計の悪い Webview は、VS Code 内で浮いて見えてしまう可能性が高いです。
Webview を使用する前に、以下の点を考慮してください
-
その機能は本当に VS Code 内にある必要がありますか?別のアプリケーションやウェブサイトにした方が良いのではないでしょうか?
-
その機能を実装する方法は Webview しかありませんか?代わりに通常の VS Code API を使用できませんか?
-
その Webview は、高いリソースコストに見合うだけのユーザー価値を提供しますか?
覚えておいてください:Webview で実現できるからといって、必ずしもそうすべきとは限りません。しかし、Webview を使う必要があると確信しているなら、このドキュメントが役立ちます。始めましょう。
Webview API の基本
Webview API を説明するために、Cat Coding というシンプルな拡張機能を作成します。この拡張機能は、Webview を使って猫が(おそらく VS Code で)コードを書いている GIF を表示します。API を学習しながら、猫が書いたソースコードの行数を追跡するカウンターや、猫がバグを混入させたときにユーザーに通知する機能などを追加していきます。
Cat Coding 拡張機能の最初のバージョンにおける package.json を示します。サンプルアプリの完全なコードはこちらにあります。拡張機能の最初のバージョンでは、catCoding.start というコマンドをコントリビュートします。ユーザーがこのコマンドを呼び出すと、猫を表示するシンプルな Webview が開きます。ユーザーはコマンドパレットから Cat Coding: Start new cat coding session としてこのコマンドを呼び出すか、必要に応じてキーバインドを設定することもできます。
{
"name": "cat-coding",
"description": "Cat Coding",
"version": "0.0.1",
"publisher": "bierner",
"engines": {
"vscode": "^1.74.0"
},
"activationEvents": [],
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "catCoding.start",
"title": "Start new cat coding session",
"category": "Cat Coding"
}
]
},
"scripts": {
"vscode:prepublish": "tsc -p ./",
"compile": "tsc -watch -p ./",
"postinstall": "node ./node_modules/vscode/bin/install"
},
"dependencies": {
"vscode": "*"
},
"devDependencies": {
"@types/node": "^9.4.6",
"typescript": "^2.8.3"
}
}
注意:拡張機能のターゲットが VS Code 1.74 より前のバージョンの場合、
activationEventsに明示的にonCommand: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 コマンドを実行すると、以下のようになります。

コマンドによって正しいタイトルの新しい 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 はこのようになります。

進展しました!
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>`;
}

webview.html を設定すると、iframe のリロードと同様に Webview のコンテンツ全体が置き換わります。Webview 内でスクリプトを使用し始めると、webview.html を設定するたびにスクリプトの状態もリセットされることに注意してください。
上記の例では webview.title も使用しており、エディターに表示されるドキュメントのタイトルを変更しています。タイトルを設定しても Webview はリロードされません。
ライフサイクル
Webview パネルは、それを作成した拡張機能が所有します。拡張機能は createWebviewPanel から返された Webview の参照を保持する必要があります。この参照を失うと、Webview 自体は VS Code 内に表示され続けていても、拡張機能は二度とアクセスできなくなります。
テキストエディターと同様に、ユーザーはいつでも 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 のコンテンツを自動的に復元します。

.visible プロパティで、Webview パネルが現在表示されているかどうかを確認できます。
reveal() を呼び出すことで、プログラム的に Webview パネルをフォアグラウンドに持ってくることができます。このメソッドには、パネルを表示するターゲットエディター列をオプションで指定できます。Webview パネルは一度に 1 つの列にしか表示できません。reveal() を呼び出したり、ドラッグして新しい列に移動したりすると、Webview はその新しい列に移動します。

Webview が一度に 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
);
}
})
);
}
新しい拡張機能の動作は以下の通りです。

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);
}

Webview の検査とデバッグ
Developer: Toggle Developer Tools コマンドは、Webview をデバッグおよび検査するための Developer Tools ウィンドウを開きます。

VS Code 1.56 より古いバージョンを使用している場合、または enableFindWidget が設定された Webview をデバッグする場合は、代わりに Developer: Open Webview Developer Tools コマンドを使用する必要があります。このコマンドは、すべての Webview とエディター自体で共有される Developer Tools ページを使用するのではなく、各 Webview 専用の Developer Tools ページを開きます。
Developer Tools から、左上隅にあるインスペクトツールを使用して Webview のコンテンツの検査を開始できます。

また、Developer Tools のコンソールで Webview からのすべてのエラーやログを確認することもできます。

Webview のコンテキストで式を評価するには、Developer Tools コンソールパネルの左上にあるドロップダウンから active frame 環境を選択するようにしてください。

active frame 環境は、Webview のスクリプト自体が実行される場所です。
さらに、Developer: Reload Webview コマンドですべてのアクティブな Webview を再読み込みできます。これは、Webview の状態をリセットする必要がある場合や、ディスク上のコンテンツが変更され、新しいコンテンツを読み込みたい場合に便利です。
ローカルコンテンツの読み込み
Webview は、ローカルリソースに直接アクセスできない分離されたコンテキストで実行されます。これはセキュリティ上の理由によるものです。つまり、画像やスタイルシート、その他のリソースを拡張機能から読み込んだり、ユーザーの現在のワークスペースからコンテンツを読み込んだりするには、Webview.asWebviewUri 関数を使用してローカルの file: URI を、VS Code がローカルリソースのサブセットを読み込むために使用できる特別な URI に変換する必要があります。
猫の GIF を Giphy から取得するのではなく、拡張機能にバンドルしたいとします。そのためには、まずディスク上のファイルへの URI を作成し、その URI を asWebviewUri 関数に通します。
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{}
);
// Get path to resource on disk
const onDiskPath = vscode.Uri.joinPath(context.extensionUri, 'media', 'cat.gif');
// And get the special URI to use with the webview
const catGifSrc = panel.webview.asWebviewUri(onDiskPath);
panel.webview.html = getWebviewContent(catGifSrc);
})
);
}
function getWebviewContent(catGifSrc: vscode.Uri) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<img src="${catGifSrc}" width="300" />
</body>
</html>`;
}
このコードをデバッグすると、catGifSrc の実際の値は以下のようになっていることがわかります。
vscode-resource:/Users/toonces/projects/vscode-cat-coding/media/cat.gif
VS Code はこの特別な URI を理解し、それを使ってディスクから GIF を読み込みます!
デフォルトでは、Webview は以下の場所にあるリソースにのみアクセスできます。
- 拡張機能のインストールディレクトリ内。
- ユーザーの現在アクティブなワークスペース内。
追加のローカルリソースへのアクセスを許可するには、WebviewOptions.localResourceRoots を使用してください。
また、データ URI を使用してリソースを Webview に直接埋め込むことも常に可能です。
ローカルリソースへのアクセス制御
Webview は localResourceRoots オプションを使用して、ユーザーのコンピューターから読み込めるリソースを制御できます。localResourceRoots は、ローカルコンテンツを読み込むことができるルート URI のセットを定義します。
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 がセキュリティのベストプラクティスに従っていることを確認し、コンテンツセキュリティポリシー (CSP) を追加して読み込めるコンテンツをさらに制限してください。
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.foreground は var(--vscode-editor-foreground) となります。
code {
color: var(--vscode-editor-foreground);
}
利用可能なテーマ変数については、テーマカラーリファレンスを確認してください。変数に対して IntelliSense による提案を提供する拡張機能も利用可能です。
フォント関連の以下の変数も定義されています。
--vscode-editor-font-family- エディターのフォントファミリー (editor.fontFamily設定より)。--vscode-editor-font-weight- エディターのフォントウェイト (editor.fontWeight設定より)。--vscode-editor-font-size- エディターのフォントサイズ (editor.fontSize設定より)。
最後に、特定のテーマのみをターゲットとする CSS を書く必要がある特殊なケースのために、Webview の body 要素には現在アクティブなテーマの 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 を使用する必要があります。
コンテキストメニュー
高度な Webview では、ユーザーが Webview 内で右クリックしたときに表示されるコンテキストメニューをカスタマイズできます。これは、VS Code の通常のコンテキストメニューと同様にコントリビューションポイントを使用して行われるため、カスタムメニューはエディターの他の部分とシームレスに統合されます。Webview は、特定のセクションごとにカスタムコンテキストメニューを表示することもできます。
Webview に新しいコンテキストメニュー項目を追加するには、まず menus の新しい webview/context セクションにエントリを追加します。各コントリビューションには command(アイテムのタイトルもここから取得されます)と when 句が必要です。when 句には、コンテキストメニューがあなたの拡張機能の Webview にのみ適用されるように 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>` の内部を右クリックすると、次のようになります。

左クリックや主クリックでメニューを表示するのが便利な場合もあります。例えば、スプリットボタンでメニューを表示する場合です。これは、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>

スクリプトとメッセージパッシング
Webview は iframe と同様にスクリプトを実行できます。JavaScript はデフォルトでは Webview 内で無効になっていますが、enableScripts: true オプションを渡すことで簡単に有効にできます。
スクリプトを使用して、猫が書いたソースコードの行数を追跡するカウンターを追加してみましょう。基本的なスクリプトの実行は非常に簡単ですが、この例はデモンストレーション用であることに注意してください。実際には、Webview は常に コンテンツセキュリティポリシー (CSP) を使用してインラインスクリプトを無効にすべきです。
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>`;
}

すごい!非常に生産的な猫ですね。
Webview スクリプトは、通常のウェブページのスクリプトができることなら何でも実行できます。ただし、Webview は独自のコンテキストに存在するため、Webview 内のスクリプトは VS Code API にアクセスできないことに注意してください。そこでメッセージパッシングの出番です!
拡張機能から Webview へのメッセージ送信
拡張機能は webview.postMessage() を使用して Webview にデータを送信できます。このメソッドは、JSON シリアライズ可能なデータを Webview に送信します。メッセージは Webview 内で標準の message イベントを通じて受信されます。
これをデモンストレーションするために、現在コーディングしている猫にコードのリファクタリングを指示(これにより行数を減らす)する新しいコマンドを Cat Coding に追加します。新しい catCoding.doRefactor コマンドは postMessage を使用して現在の Webview に指示を送り、Webview 自体は window.addEventListener('message', event => { ... }) を使用してメッセージを処理します。
export function activate(context: vscode.ExtensionContext) {
// Only allow a single Cat Coder
let currentPanel: vscode.WebviewPanel | undefined = undefined;
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
if (currentPanel) {
currentPanel.reveal(vscode.ViewColumn.One);
} else {
currentPanel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{
enableScripts: true
}
);
currentPanel.webview.html = getWebviewContent();
currentPanel.onDidDispose(
() => {
currentPanel = undefined;
},
undefined,
context.subscriptions
);
}
})
);
// Our new command
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.doRefactor', () => {
if (!currentPanel) {
return;
}
// Send a message to our webview.
// You can send any JSON serializable data.
currentPanel.webview.postMessage({ command: 'refactor' });
})
);
}
function getWebviewContent() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
<h1 id="lines-of-code-counter">0</h1>
<script>
const counter = document.getElementById('lines-of-code-counter');
let count = 0;
setInterval(() => {
counter.textContent = count++;
}, 100);
// Handle the message inside the webview
window.addEventListener('message', event => {
const message = event.data; // The JSON data our extension sent
switch (message.command) {
case 'refactor':
count = Math.ceil(count * 0.5);
counter.textContent = count;
break;
}
});
</script>
</body>
</html>`;
}

Webview から拡張機能へのメッセージ送信
Webview は拡張機能にメッセージを送り返すこともできます。これは、Webview 内の特別な VS Code API オブジェクトにある postMessage 関数を使用して行われます。VS Code API オブジェクトにアクセスするには、Webview 内で acquireVsCodeApi を呼び出します。この関数はセッションごとに一度だけ呼び出すことができます。このメソッドが返す VS Code API インスタンスを保持し、それを使用する必要がある他の関数に渡す必要があります。
Cat Coding Webview で VS Code API と postMessage を使用して、猫がコードにバグを混入させたときに拡張機能にアラートを送ることができます。
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{
enableScripts: true
}
);
panel.webview.html = getWebviewContent();
// Handle messages from the webview
panel.webview.onDidReceiveMessage(
message => {
switch (message.command) {
case 'alert':
vscode.window.showErrorMessage(message.text);
return;
}
},
undefined,
context.subscriptions
);
})
);
}
function getWebviewContent() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
<h1 id="lines-of-code-counter">0</h1>
<script>
(function() {
const vscode = acquireVsCodeApi();
const counter = document.getElementById('lines-of-code-counter');
let count = 0;
setInterval(() => {
counter.textContent = count++;
// Alert the extension when our cat introduces a bug
if (Math.random() < 0.001 * count) {
vscode.postMessage({
command: 'alert',
text: '🐛 on line ' + count
})
}
}, 100);
}())
</script>
</body>
</html>`;
}

セキュリティ上の理由から、VS Code API オブジェクトをプライベートに保ち、グローバルスコープに漏洩させないように注意してください。
Web Worker の使用
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 スクリプトは、importScripts や import(...) を使用したソースコードのインポートをサポートしていません。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)] または、すべてのローカルリソースへのアクセスを禁止するために [] に設定してください。
コンテンツセキュリティポリシー (CSP)
コンテンツセキュリティポリシーは、Webview で読み込みおよび実行できるコンテンツをさらに制限します。例えば、CSP を使用すると、許可されたスクリプトのみを実行するようにしたり、画像を 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 で外部リソースの読み込みを許可する場合、それらのリソースは http ではなく https 経由でのみ読み込まれるようにすることを強く推奨します。上記のコンテンツセキュリティポリシーの例では、画像のみを https: で読み込めるようにすることで、すでにこれを実現しています。
すべてのユーザー入力をサニタイズする
通常のウェブページの場合と同様に、Webview の HTML を作成する際はすべてのユーザー入力をサニタイズする必要があります。入力を適切にサニタイズしないと、コンテンツインジェクションを許し、ユーザーをセキュリティリスクにさらす可能性があります。
サニタイズが必要な値の例
- ファイルの内容。
- ファイルおよびフォルダーのパス。
- ユーザー設定およびワークスペース設定。
HTML 文字列を構築するためにヘルパーライブラリの使用を検討するか、少なくともワークスペースからのすべてのコンテンツが適切にサニタイズされていることを確認してください。
サニタイズのみに依存しないでください。潜在的なコンテンツインジェクションの影響を最小限に抑えるために、コンテンツセキュリティポリシー (CSP) を持つなどの他のセキュリティベストプラクティスにも必ず従ってください。
永続性
標準の Webview ライフサイクルでは、Webview は createWebviewPanel によって作成され、ユーザーが閉じるか .dispose() が呼び出されたときに破棄されます。しかし、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);
getState と setState は、retainContextWhenHidden よりもパフォーマンスオーバーヘッドがはるかに低いため、状態を永続化するための推奨される方法です。
シリアライズ
WebviewPanelSerializer を実装することで、VS Code の再起動時に Webview を自動的に復元できます。シリアライズは getState と setState を基盤としており、拡張機能が Webview に対して WebviewPanelSerializer を登録した場合にのみ有効になります。
コーディングする猫を VS Code の再起動をまたいで永続化させるには、まず拡張機能の package.json に onWebviewPanel アクティベーションイベントを追加します。
"activationEvents": [
...,
"onWebviewPanel:catCoding"
]
このアクティベーションイベントは、VS Code が catCoding という viewType の 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 自体がフォアグラウンドになくても、コンテンツを保持したまま非表示状態にできます。
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>`;
}

Webview が非表示になってから復元されても、カウンターがリセットされないことに注目してください。追加のコードは不要です!retainContextWhenHidden を使用すると、Webview はウェブブラウザのバックグラウンドタブと同様に動作します。タブがアクティブまたは表示されていなくても、スクリプトやその他の動的なコンテンツは実行され続けます。また、retainContextWhenHidden が有効な場合は、非表示の Webview にメッセージを送信することもできます。
retainContextWhenHidden は魅力的ですが、メモリ消費量が非常に高いため、他の永続化手法が機能しない場合にのみ使用すべきであることに留意してください。
アクセシビリティ
ユーザーがスクリーンリーダーを使用して VS Code を操作しているコンテキストでは、Webview のメインボディにクラス vscode-using-screen-reader が追加されます。さらに、ユーザーがウィンドウ内の動きを減らすことを希望している場合、ドキュメントのメインボディ要素にクラス vscode-reduce-motion が追加されます。これらのクラスを監視してレンダリングを調整することで、Webview コンテンツでユーザーの好みをよりよく反映させることができます。
次のステップ
VS Code の拡張性についてさらに詳しく学びたい場合は、以下のトピックを試してみてください。
- Extension API - VS Code Extension API の詳細を学びます。
- Extension Capabilities - VS Code を拡張する他の方法を確認します。