Webview API
Webview API を使用すると、拡張機能は Visual Studio Code 内に完全にカスタマイズ可能なビューを作成できます。たとえば、組み込みの Markdown 拡張機能は Webview を使用して Markdown プレビューをレンダリングします。Webview は、VS Code のネイティブ API がサポートする範囲を超えた複雑なユーザー インターフェイスを構築するためにも使用できます。
Webview は、拡張機能が制御する VS Code 内の iframe
と考えてください。Webview は、このフレームにほとんどすべての HTML コンテンツをレンダリングでき、メッセージパッシングを使用して拡張機能と通信します。この自由さにより、Webview は信じられないほど強力になり、拡張機能の可能性のまったく新しい範囲が開かれます。
Webview は、いくつかの VS Code API で使用されます。
createWebviewPanel
を使用して作成された Webview パネルを使用します。この場合、Webview パネルは VS Code で個別のエディターとして表示されます。そのため、カスタム UI やカスタム視覚化の表示に役立ちます。- カスタム エディターのビューとして。カスタム エディターを使用すると、拡張機能はワークスペース内の任意のファイルを編集するためのカスタム UI を提供できます。カスタム エディター API は、元に戻す/やり直しなどのエディター イベントや、保存などのファイル イベントに拡張機能をフックすることもできます。
- サイドバーまたはパネル領域にレンダリングされるWebview ビューにあります。詳細については、Webview ビュー サンプル拡張機能を参照してください。
このページでは、基本的な Webview パネル API に焦点を当てていますが、ここで説明するほとんどすべてが、カスタム エディターや Webview ビューで使用される Webview にも適用されます。これらの API により興味がある場合でも、まずこのページを読んで Webview の基本を理解することをお勧めします。
リンク
VS Code API の使用
Webview を使用すべきでしょうか?
Webview は非常に優れていますが、VS Code のネイティブ API が不十分な場合にのみ、控えめに使用する必要があります。Webview はリソースを大量に消費し、通常の拡張機能とは別のコンテキストで実行されます。適切に設計されていない Webview は、VS Code 内で場違いに感じられることもあります。
Webview を使用する前に、以下を考慮してください。
-
この機能は本当に VS Code 内に存在する必要がありますか?別のアプリケーションや Web サイトの方が良いのではないでしょうか?
-
Webview が機能を実装する唯一の方法ですか?代わりに通常の VS Code API を使用できませんか?
-
Webview は、その高いリソースコストを正当化するのに十分なユーザー価値を追加しますか?
覚えておいてください:Webview で何かできるからといって、すべきではありません。ただし、Webview を使用する必要があると確信している場合は、このドキュメントが役立ちます。始めましょう。
Webview API の基本
Webview API を説明するために、Cat Coding というシンプルな拡張機能を作成します。この拡張機能は、Webview を使用して、コードを書く猫の GIF (おそらく VS Code で) を表示します。API を使用しながら、猫が書いたソースコードの行数を追跡するカウンターや、猫がバグを導入したときにユーザーに通知する機能など、拡張機能に機能を追加し続けます。
Cat Coding 拡張機能の最初のバージョンの package.json
は次のとおりです。サンプルアプリの完全なコードはこちらで確認できます。拡張機能の最初のバージョンは、catCoding.start
というコマンドを提供します。ユーザーがこのコマンドを呼び出すと、猫がいるシンプルな Webview を表示します。ユーザーは、コマンド パレットからCat Coding: 新しい猫のコーディング セッションを開始としてこのコマンドを呼び出すことができ、必要であればキーバインディングを作成することもできます。
{
"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
コマンドを実装しましょう。拡張機能のメインファイルで、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 自体は破棄されます。破棄された 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 つの editor 列にのみ表示できます。reveal()
を呼び出すか、Webview パネルを新しい editor 列にドラッグすると、Webview はその新しい列に移動します。
一度に 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
);
}
})
);
}
新しい拡張機能の動作は次のとおりです。
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);
}
Webview の検査とデバッグ
開発者: 開発者ツールを切り替えるコマンドは、Webview のデバッグと検査に使用できる開発者ツールウィンドウを開きます。
VS Code のバージョンが 1.56 より古い場合、または enableFindWidget
を設定する Webview をデバッグしようとしている場合は、代わりに開発者: Webview 開発者ツールを開くコマンドを使用する必要があることに注意してください。このコマンドは、すべての Webview とエディター自体で共有される開発者ツールページを使用するのではなく、各 Webview 専用の開発者ツールページを開きます。
開発者ツールから、開発者ツールウィンドウの左上隅にある検査ツールを使用して Webview のコンテンツの検査を開始できます。
開発者ツールのコンソールで、Webview からのすべてのエラーとログを表示することもできます。
Webview のコンテキストで式を評価するには、開発者ツールのコンソールパネルの左上隅にあるドロップダウンからアクティブ フレーム環境を選択していることを確認してください。
アクティブ フレーム環境は、Webview スクリプト自体が実行される場所です。
さらに、開発者: Webview を再読み込みするコマンドは、アクティブなすべての 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 がセキュリティのベストプラクティスにも従い、読み込めるコンテンツをさらに制限するためにコンテンツセキュリティポリシーを追加するようにしてください。
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 要素に vscode-theme-id
というデータ属性があり、現在アクティブなテーマの ID が格納されます。これにより、Webview 用のテーマ固有の CSS を記述できます。
body[data-vscode-theme-id="One Dark Pro"] {
background: hotpink;
}
サポートされているメディア形式
Webview はオーディオとビデオをサポートしていますが、すべてのメディアコーデックまたはメディアファイルコンテナタイプがサポートされているわけではありません。
Webview で使用できるオーディオ形式は次のとおりです。
- Wav
- Mp3
- Ogg
- Flac
Webview で使用できるビデオ形式は次のとおりです。
- H.264
- VP8
ビデオファイルの場合、ビデオとオーディオトラックの両方のメディア形式がサポートされていることを確認してください。たとえば、多くの .mp4
ファイルはビデオに H.264
、オーディオに AAC
を使用します。VS Code は mp4
のビデオ部分を再生できますが、AAC
オーディオはサポートされていないため、音は出ません。代わりに、オーディオトラックには mp3
を使用する必要があります。
コンテキストメニュー
高度な Webview では、ユーザーが Webview 内を右クリックしたときに表示されるコンテキストメニューをカスタマイズできます。これは、VS Code の通常のコンテキストメニューと同様に、コントリビューションポイントを使用して行われるため、カスタムメニューはエディターの他の部分とうまく統合されます。Webview は、Webview の異なるセクションにカスタムコンテキストメニューを表示することもできます。
Webview に新しいコンテキストメニュー項目を追加するには、まず新しい webview/context
セクションの下の menus
に新しいエントリを追加します。各コントリビューションは command
(項目のタイトルもここから来ます) と when
句を受け取ります。when 句には webviewId == 'YOUR_WEBVIEW_VIEW_TYPE'
を含め、コンテキストメニューが拡張機能の Webview にのみ適用されるようにする必要があります。
"contributes": {
"menus": {
"webview/context": [
{
"command": "catCoding.yarn",
"when": "webviewId == 'catCoding'"
},
{
"command": "catCoding.insertLion",
"when": "webviewId == 'catCoding' && webviewSection == 'editor'"
}
]
},
"commands": [
{
"command": "catCoding.yarn",
"title": "Yarn 🧶",
"category": "Cat Coding"
},
{
"command": "catCoding.insertLion",
"title": "Insert 🦁",
"category": "Cat Coding"
},
...
]
}
Webview 内では、data-vscode-context
データ属性 (または JavaScript では dataset.vscodeContext
) を使用して、HTML の特定の領域のコンテキストを設定することもできます。data-vscode-context
の値は、ユーザーが要素を右クリックしたときに設定するコンテキストを指定する JSON オブジェクトです。最終的なコンテキストは、ドキュメントルートからクリックされた要素に移動して決定されます。
例えば、このHTMLを考えてみましょう。
<div class="main" data-vscode-context='{"webviewSection": "main", "mouseCount": 4}'>
<h1>Cat Coding</h1>
<textarea data-vscode-context='{"webviewSection": "editor", "preventDefaultContextMenuItems": true}'></textarea>
</div>
ユーザーが textarea
を右クリックすると、以下のコンテキストが設定されます。
webviewSection == 'editor'
- これは親要素からwebviewSection
を上書きします。mouseCount == 4
- これは親要素から継承されます。preventDefaultContextMenuItems == true
- これは、VS Code が通常 Webview コンテキストメニューに追加するコピーおよびペーストのエントリを非表示にする特殊なコンテキストです。
ユーザーが<textarea>
内で右クリックすると、次のように表示されます。
左クリック/主クリックでメニューを表示すると便利な場合があります。たとえば、分割ボタンでメニューを表示する場合などです。これは、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 と同じであり、スクリプトを実行することもできます。Webview では JavaScript はデフォルトで無効になっていますが、enableScripts: true
オプションを渡すことで簡単に再有効化できます。
スクリプトを使用して、猫が書いたソースコードの行数を追跡するカウンターを追加しましょう。基本的なスクリプトの実行は非常に簡単ですが、この例はデモンストレーション目的のみであることに注意してください。実際には、Webview は常にコンテンツセキュリティポリシーを使用してインラインスクリプトを無効にする必要があります。
import * as path from 'path';
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{
// Enable scripts in the webview
enableScripts: true
}
);
panel.webview.html = getWebviewContent();
})
);
}
function getWebviewContent() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
<h1 id="lines-of-code-counter">0</h1>
<script>
const counter = document.getElementById('lines-of-code-counter');
let count = 0;
setInterval(() => {
counter.textContent = count++;
}, 100);
</script>
</body>
</html>`;
}
すごい!なんて生産的な猫でしょう。
Webview のスクリプトは、通常の Web ページ上のスクリプトとほぼ同じことができます。ただし、Webview は独自のコンテキストで存在するため、Webview 内のスクリプトは VS Code API にアクセスできないことに注意してください。ここでメッセージパッシングが登場します!
拡張機能から Webview へのメッセージ送信
拡張機能は、webview.postMessage()
を使用して Webview にデータを送信できます。このメソッドは、任意の JSON シリアライズ可能なデータを Webview に送信します。メッセージは、標準の message
イベントを通じて Webview 内で受信されます。
これを実証するために、現在コーディング中の猫にコードをリファクタリングする (それによって合計行数を減らす) ように指示する新しいコマンドをCat Codingに追加しましょう。新しい catCoding.doRefactor
コマンドは、postMessage
を使用して現在の Webview に指示を送信し、Webview 自体内で window.addEventListener('message', event => { ... })
を使用してメッセージを処理します。
export function activate(context: vscode.ExtensionContext) {
// Only allow a single Cat Coder
let currentPanel: vscode.WebviewPanel | undefined = undefined;
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
if (currentPanel) {
currentPanel.reveal(vscode.ViewColumn.One);
} else {
currentPanel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{
enableScripts: true
}
);
currentPanel.webview.html = getWebviewContent();
currentPanel.onDidDispose(
() => {
currentPanel = undefined;
},
undefined,
context.subscriptions
);
}
})
);
// Our new command
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.doRefactor', () => {
if (!currentPanel) {
return;
}
// Send a message to our webview.
// You can send any JSON serializable data.
currentPanel.webview.postMessage({ command: 'refactor' });
})
);
}
function getWebviewContent() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
<h1 id="lines-of-code-counter">0</h1>
<script>
const counter = document.getElementById('lines-of-code-counter');
let count = 0;
setInterval(() => {
counter.textContent = count++;
}, 100);
// Handle the message inside the webview
window.addEventListener('message', event => {
const message = event.data; // The JSON data our extension sent
switch (message.command) {
case 'refactor':
count = Math.ceil(count * 0.5);
counter.textContent = count;
break;
}
});
</script>
</body>
</html>`;
}
Webview から拡張機能へのメッセージ送信
Webview は、メッセージを拡張機能に送り返すこともできます。これは、Webview 内の特別な VS Code API オブジェクトの postMessage
関数を使用して実現されます。VS Code API オブジェクトにアクセスするには、Webview 内で acquireVsCodeApi
を呼び出します。この関数は、セッションごとに一度だけ呼び出すことができます。このメソッドによって返された VS Code API のインスタンスを保持し、それを使用する必要がある他の関数に渡す必要があります。
VS Code API と postMessage
をCat Coding Webview で使用して、猫がコードにバグを導入したときに拡張機能に警告することができます。
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{
enableScripts: true
}
);
panel.webview.html = getWebviewContent();
// Handle messages from the webview
panel.webview.onDidReceiveMessage(
message => {
switch (message.command) {
case 'alert':
vscode.window.showErrorMessage(message.text);
return;
}
},
undefined,
context.subscriptions
);
})
);
}
function getWebviewContent() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
<h1 id="lines-of-code-counter">0</h1>
<script>
(function() {
const vscode = acquireVsCodeApi();
const counter = document.getElementById('lines-of-code-counter');
let count = 0;
setInterval(() => {
counter.textContent = count++;
// Alert the extension when our cat introduces a bug
if (Math.random() < 0.001 * count) {
vscode.postMessage({
command: 'alert',
text: '🐛 on line ' + count
})
}
}, 100);
}())
</script>
</body>
</html>`;
}
セキュリティ上の理由から、VS Code API オブジェクトはプライベートに保ち、グローバルスコープに漏洩しないようにする必要があります。
Web Workers の使用
Web Workers は Webview 内でサポートされていますが、注意すべきいくつかの重要な制限があります。
まず、ワーカーは data:
または blob:
URI を使用してのみ読み込むことができます。拡張機能のフォルダーからワーカーを直接読み込むことはできません。
拡張機能内の JavaScript ファイルからワーカーコードを読み込む必要がある場合は、fetch
を使用してみてください。
const workerSource = 'absolute/path/to/worker.js';
fetch(workerSource)
.then(result => result.blob())
.then(blob => {
const blobUrl = URL.createObjectURL(blob);
new Worker(blobUrl);
});
また、ワーカー スクリプトは importScripts
または import(...)
を使用したソース コードのインポートをサポートしていません。ワーカーがコードを動的にロードする場合、webpack などのバンドラーを使用してワーカー スクリプトを単一のファイルにパッケージ化してみてください。
webpack
を使用すると、LimitChunkCountPlugin
を使用して、コンパイルされたワーカー JavaScript を単一のファイルに強制することができます。
const path = require('path');
const webpack = require('webpack');
module.exports = {
target: 'webworker',
entry: './worker/src/index.js',
output: {
filename: 'worker.js',
path: path.resolve(__dirname, 'media')
},
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1
})
]
};
セキュリティ
あらゆるウェブページと同様に、ウェブビューを作成する際には、基本的なセキュリティのベストプラクティスに従う必要があります。
機能の制限
Webview は、必要最小限の機能セットを持つべきです。たとえば、Webview がスクリプトを実行する必要がない場合、enableScripts: true
を設定しないでください。Webview がユーザーのワークスペースからリソースを読み込む必要がない場合、localResourceRoots
を [vscode.Uri.file(extensionContext.extensionPath)]
または []
に設定して、すべてのローカルリソースへのアクセスを禁止してください。
コンテンツセキュリティポリシー
コンテンツセキュリティポリシーは、Webview で読み込みおよび実行できるコンテンツをさらに制限します。たとえば、コンテンツセキュリティポリシーは、許可されたスクリプトのリストのみが Webview で実行されるようにしたり、Webview に https
経由で画像のみを読み込むように指示したりできます。
コンテンツセキュリティポリシーを追加するには、Webview の <head>
の先頭に <meta http-equiv="Content-Security-Policy">
ディレクティブを配置します。
function getWebviewContent() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
...
</body>
</html>`;
}
ポリシー default-src 'none';
はすべてのコンテンツを禁止します。次に、拡張機能が機能するために必要な最小限のコンテンツを再度有効にできます。以下は、ローカルスクリプトとスタイルシートの読み込み、および https
経由での画像の読み込みを許可するコンテンツセキュリティポリシーです。
<meta
http-equiv="Content-Security-Policy"
content="default-src 'none'; img-src ${webview.cspSource} https:; script-src ${webview.cspSource}; style-src ${webview.cspSource};"
/>
${webview.cspSource}
の値は、webview オブジェクト自体から来る値のプレースホルダーです。この値の完全な使用例については、webview サンプルを参照してください。
このコンテンツセキュリティポリシーは、暗黙的にインラインスクリプトとスタイルも無効にします。すべてのインラインスタイルとスクリプトを外部ファイルに抽出し、コンテンツセキュリティポリシーを緩和せずに適切に読み込めるようにすることがベストプラクティスです。
https 経由でのみコンテンツを読み込む
Webview が外部リソースの読み込みを許可する場合、これらのリソースを https
経由でのみ読み込み、http 経由では読み込まないことを強くお勧めします。上記のコンテンツセキュリティポリシーの例は、https:
経由でのみ画像の読み込みを許可することで、これを既に実現しています。
すべてのユーザー入力をサニタイズする
通常のウェブページと同様に、ウェブビューの 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);
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 自体がフォアグラウンドに表示されていなくても、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 は Web ブラウザのバックグラウンドタブと同様に動作します。スクリプトやその他の動的コンテンツは、タブがアクティブまたは表示されていない場合でも実行され続けます。retainContextWhenHidden
が有効になっている場合、非表示の Webview にメッセージを送信することもできます。
retainContextWhenHidden
は魅力的かもしれませんが、メモリオーバーヘッドが高く、他の永続化技術が機能しない場合にのみ使用すべきであることに注意してください。
アクセシビリティ
ユーザーがスクリーンリーダーを使用して VS Code を操作しているコンテキストでは、クラス vscode-using-screen-reader
が Webview のメインボディに追加されます。さらに、ユーザーがウィンドウ内の動きの量を減らすことを希望している場合、クラス vscode-reduce-motion
がドキュメントのメインボディ要素に追加されます。これらのクラスを観察し、それに応じてレンダリングを調整することで、Webview のコンテンツはユーザーの好みをよりよく反映できます。
次のステップ
VS Code の拡張性についてさらに詳しく知りたい場合は、以下のトピックを試してみてください。