Webview API
Webview APIを使用すると、拡張機能はVisual Studio Code内で完全にカスタマイズ可能なビューを作成できます。例えば、組み込みのMarkdown拡張機能はwebviewsを使用してMarkdownプレビューをレンダリングします。Webviewsは、VS CodeのネイティブAPIがサポートする範囲を超えた複雑なユーザーインターフェースを構築するためにも使用できます。
webviewは、拡張機能が制御するVS Code内のiframe
と考えてください。webviewはこのフレーム内でほぼすべてのHTMLコンテンツをレンダリングでき、メッセージパッシングを使用して拡張機能と通信します。この自由度により、webviewsは信じられないほど強力になり、拡張機能の可能性をまったく新しい範囲に広げます。
WebviewはいくつかのVS Code APIで使用されています
createWebviewPanel
を使用して作成されたWebviewパネル。この場合、WebviewパネルはVS Codeで個別のエディターとして表示されます。これにより、カスタムUIやカスタムビジュアライゼーションの表示に役立ちます。- カスタムエディターのビューとして。カスタムエディターを使用すると、拡張機能はワークスペース内の任意のファイルを編集するためのカスタムUIを提供できます。カスタムエディターAPIは、元に戻すややり直しなどのエディターイベント、および保存などのファイルイベントに拡張機能がフックすることも可能にします。
- サイドバーまたはパネル領域にレンダリングされるWebviewビューとして。詳細については、webviewビューサンプル拡張機能を参照してください。
このページでは基本的なwebviewパネルAPIに焦点を当てていますが、ここで説明するほとんどすべてはカスタムエディターやwebviewビューで使用されるwebviewsにも適用されます。これらのAPIにさらに興味がある場合でも、まずこのページを読んでwebviewの基本に慣れることをお勧めします。
リンク
VS Code APIの使用法
Webviewを使用すべきですか?
Webviewは非常に素晴らしいですが、VS CodeのネイティブAPIが不十分な場合にのみ、控えめに使用すべきです。Webviewはリソースを多く消費し、通常の拡張機能とは別のコンテキストで実行されます。設計が不適切なWebviewは、VS Code内で簡単に場違いに感じられることもあります。
Webviewを使用する前に、以下の点を考慮してください。
-
この機能は本当にVS Code内で動作する必要がありますか?別のアプリケーションやウェブサイトとしての方が良いのではないでしょうか?
-
Webviewが機能を実装する唯一の方法ですか?代わりに通常のVS Code APIを使用できますか?
-
あなたのWebviewは、高いリソースコストを正当化するのに十分なユーザー価値を提供しますか?
覚えておいてください:Webviewで何かできるからといって、すべきであるとは限りません。しかし、Webviewを使用する必要があると確信している場合は、このドキュメントがお役に立ちます。始めましょう。
Webview APIの基本
webview APIを説明するために、Cat Codingというシンプルな拡張機能を作成します。この拡張機能は、webviewを使用して、猫がコードを書いている(おそらくVS Codeで)GIFを表示します。APIを進めるにつれて、猫が書いたソースコードの行数を追跡するカウンターや、猫がバグを導入したときにユーザーに通知する機能など、拡張機能に機能を追加し続けます。
Cat Coding拡張機能の最初のバージョンのpackage.json
を以下に示します。サンプルアプリの完全なコードはこちらで確認できます。拡張機能の最初のバージョンは、catCoding.start
というコマンドを提供します。ユーザーがこのコマンドを呼び出すと、猫が登場するシンプルなwebviewが表示されます。ユーザーはコマンドパレットからCat Coding: Start new cat coding sessionとしてこのコマンドを呼び出すことができ、必要であればキーバインディングを作成することも可能です。
{
"name": "cat-coding",
"description": "Cat Coding",
"version": "0.0.1",
"publisher": "bierner",
"engines": {
"vscode": "^1.74.0"
},
"activationEvents": [],
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "catCoding.start",
"title": "Start new cat coding session",
"category": "Cat Coding"
}
]
},
"scripts": {
"vscode:prepublish": "tsc -p ./",
"compile": "tsc -watch -p ./",
"postinstall": "node ./node_modules/vscode/bin/install"
},
"dependencies": {
"vscode": "*"
},
"devDependencies": {
"@types/node": "^9.4.6",
"typescript": "^2.8.3"
}
}
注: 拡張機能がVS Codeのバージョン1.74より前を対象としている場合、
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つのエディター列にのみ表示できます。reveal()
を呼び出すか、webviewパネルを新しいエディター列にドラッグすると、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の検査とデバッグ
Developer: Toggle Developer Toolsコマンドを実行すると、Developer Toolsウィンドウが開き、webviewsのデバッグと検査に使用できます。
VS Codeのバージョン1.56より古いものを使用している場合、またはenableFindWidget
を設定するwebviewをデバッグしようとしている場合は、代わりにDeveloper: Open Webview Developer Toolsコマンドを使用する必要があることに注意してください。このコマンドは、すべてのwebviewsとエディター自体で共有されるDeveloper Toolsページを使用するのではなく、各webview専用のDeveloper Toolsページを開きます。
Developer Toolsから、Developer Toolsウィンドウの左上隅にある検査ツールを使用してwebviewのコンテンツを検査し始めることができます。
また、開発者ツールのコンソールでwebviewからのすべてのエラーとログを表示することもできます。
webviewのコンテキストで式を評価するには、開発者ツールのコンソールパネルの左上隅にあるドロップダウンからアクティブフレーム環境を選択していることを確認してください。
アクティブフレーム環境は、webviewスクリプト自体が実行される場所です。
さらに、Developer: Reload Webviewコマンドは、すべてのアクティブなwebviewsを再ロードします。これは、webviewの状態をリセットする必要がある場合や、ディスク上のwebviewコンテンツが変更され、新しいコンテンツをロードしたい場合に役立ちます。
ローカルコンテンツのロード
Webviewは、ローカルリソースに直接アクセスできない隔離されたコンテキストで実行されます。これはセキュリティ上の理由で行われます。つまり、拡張機能から画像、スタイルシート、その他のリソースをロードしたり、ユーザーの現在のワークスペースからコンテンツをロードしたりするには、Webview.asWebviewUri
関数を使用して、ローカルのfile:
URIを、VS Codeがローカルリソースのサブセットをロードするために使用できる特別なURIに変換する必要があります。
Giphyから取得するのではなく、猫のGIFを拡張機能にバンドルし始めたいと想像してください。これを行うには、まずディスク上のファイルへのURIを作成し、次にこれらのURIをasWebviewUri
関数に通します。
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{}
);
// Get path to resource on disk
const onDiskPath = vscode.Uri.joinPath(context.extensionUri, 'media', 'cat.gif');
// And get the special URI to use with the webview
const catGifSrc = panel.webview.asWebviewUri(onDiskPath);
panel.webview.html = getWebviewContent(catGifSrc);
})
);
}
function getWebviewContent(catGifSrc: vscode.Uri) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<img src="${catGifSrc}" width="300" />
</body>
</html>`;
}
このコードをデバッグすると、catGifSrc
の実際の値が次のようになることがわかります。
vscode-resource:/Users/toonces/projects/vscode-cat-coding/media/cat.gif
VS Codeはこの特殊なURIを理解し、それを使用してディスクからGIFをロードします!
デフォルトでは、webviewsは以下の場所にあるリソースにのみアクセスできます。
- 拡張機能のインストールディレクトリ内。
- ユーザーの現在アクティブなワークスペース内。
追加のローカルリソースへのアクセスを許可するには、WebviewOptions.localResourceRoots
を使用します。
また、データURIを使用してリソースをwebviewに直接埋め込むことも常に可能です。
ローカルリソースへのアクセスを制御する
Webviewsは、localResourceRoots
オプションを使用して、ユーザーのマシンからロードできるリソースを制御できます。localResourceRoots
は、ローカルコンテンツがロードされる可能性のあるルートURIのセットを定義します。
localResourceRoots
を使用して、Cat Coding webviewsが拡張機能内のmedia
ディレクトリからのみリソースをロードするように制限できます。
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{
// Only allow the webview to access resources in our extension's media directory
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'media')]
}
);
const onDiskPath = vscode.Uri.joinPath(context.extensionUri, 'media', 'cat.gif');
const catGifSrc = panel.webview.asWebviewUri(onDiskPath);
panel.webview.html = getWebviewContent(catGifSrc);
})
);
}
すべてのローカルリソースを禁止するには、localResourceRoots
を[]
に設定するだけです。
一般的に、webviewsはローカルリソースのロードにおいて可能な限り制限的であるべきです。ただし、localResourceRoots
自体は完全なセキュリティ保護を提供するものではないことに留意してください。webviewがセキュリティのベストプラクティスに従っていること、およびロード可能なコンテンツをさらに制限するためにコンテンツセキュリティポリシーを追加していることを確認してください。
Webviewコンテンツのテーマ設定
Webviewは、CSSを使用してVS Codeの現在のテーマに基づいて外観を変更できます。VS Codeはテーマを3つのカテゴリに分類し、現在のテーマを示すためにbody
要素に特別なクラスを追加します。
vscode-light
- 明るいテーマ。vscode-dark
- 暗いテーマ。vscode-high-contrast
- ハイコントラストテーマ。
以下のCSSは、ユーザーの現在のテーマに基づいてwebviewのテキストの色を変更します。
body.vscode-light {
color: black;
}
body.vscode-dark {
color: white;
}
body.vscode-high-contrast {
color: red;
}
webviewアプリケーションを開発する際は、3種類のテーマすべてで機能することを確認してください。視覚障害のある人が利用できるように、常にハイコントラストモードでwebviewをテストしてください。
Webviewは、CSS変数を使用してVS Codeのテーマカラーにアクセスすることもできます。これらの変数名はvscode
をプレフィックスとし、.
を-
に置き換えます。例えば、editor.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を記述する必要がある特殊なケースでは、webviewsのbody要素には、現在アクティブなテーマのIDを格納するvscode-theme-id
というデータ属性があります。これにより、webviewにテーマ固有のCSSを記述できます。
body[data-vscode-theme-id="One Dark Pro"] {
background: hotpink;
}
サポートされているメディア形式
Webviewはオーディオとビデオをサポートしていますが、すべてのメディアコーデックまたはメディアファイルコンテナタイプがサポートされているわけではありません。
Webviewで使用できるオーディオ形式は以下のとおりです。
- Wav
- Mp3
- Ogg
- Flac
Webviewで使用できるビデオ形式は以下のとおりです。
- H.264
- VP8
ビデオファイルの場合、ビデオとオーディオトラックの両方のメディア形式がサポートされていることを確認してください。例えば、多くの.mp4
ファイルはビデオにH.264
、オーディオにAAC
を使用しています。VS Codeはmp4
のビデオ部分を再生できますが、AAC
オーディオはサポートされていないため、音声は出ません。代わりに、オーディオトラックにはmp3
を使用する必要があります。
コンテキストメニュー
高度なwebviewsは、ユーザーがwebview内で右クリックしたときに表示されるコンテキストメニューをカスタマイズできます。これは、VS Codeの通常のコンテキストメニューと同様に寄与点を使用して行われるため、カスタムメニューはエディターの他の部分とうまく調和します。Webviewは、webviewの異なるセクションに対してカスタムコンテキストメニューを表示することもできます。
webviewに新しいコンテキストメニュー項目を追加するには、まず新しいwebview/context
セクションの下のmenus
に新しいエントリを追加します。各寄与にはcommand
(項目名もここから来ます)とwhen
句が必要です。when句には、コンテキストメニューが拡張機能のwebviewsにのみ適用されるように、webviewId == 'YOUR_WEBVIEW_VIEW_TYPE'
を含める必要があります。
"contributes": {
"menus": {
"webview/context": [
{
"command": "catCoding.yarn",
"when": "webviewId == 'catCoding'"
},
{
"command": "catCoding.insertLion",
"when": "webviewId == 'catCoding' && webviewSection == 'editor'"
}
]
},
"commands": [
{
"command": "catCoding.yarn",
"title": "Yarn 🧶",
"category": "Cat Coding"
},
{
"command": "catCoding.insertLion",
"title": "Insert 🦁",
"category": "Cat Coding"
},
...
]
}
webview内で、data-vscode-context
データ属性(またはJavaScriptではdataset.vscodeContext
)を使用して、HTMLの特定の領域のコンテキストを設定することもできます。data-vscode-context
の値は、ユーザーが要素を右クリックしたときに設定するコンテキストを指定するJSONオブジェクトです。最終的なコンテキストは、ドキュメントルートからクリックされた要素にたどって決定されます。
例えば、このHTMLを考えてみましょう。
<div class="main" data-vscode-context='{"webviewSection": "main", "mouseCount": 4}'>
<h1>Cat Coding</h1>
<textarea data-vscode-context='{"webviewSection": "editor", "preventDefaultContextMenuItems": true}'></textarea>
</div>
ユーザーがtextarea
を右クリックすると、以下のコンテキストが設定されます。
webviewSection == 'editor'
- これは親要素からのwebviewSection
を上書きします。mouseCount == 4
- これは親要素から継承されます。preventDefaultContextMenuItems == true
- これは、VS Codeが通常webviewコンテキストメニューに追加するコピー&ペーストのエントリを非表示にする特殊なコンテキストです。
ユーザーが<textarea>
内で右クリックすると、次のように表示されます。
左クリック/プライマリクリックでメニューを表示すると便利な場合があります。例えば、分割ボタンにメニューを表示する場合などです。これは、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はデフォルトでwebviewsでは無効になっていますが、enableScripts: true
オプションを渡すことで簡単に再有効化できます。
スクリプトを使用して、猫が書いたソースコードの行数を追跡するカウンターを追加してみましょう。基本的なスクリプトを実行するのは非常に簡単ですが、この例はデモンストレーション目的のみであることを注意してください。実際には、webviewは常にコンテンツセキュリティポリシーを使用してインラインスクリプトを無効にするべきです。
import * as path from 'path';
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{
// Enable scripts in the webview
enableScripts: true
}
);
panel.webview.html = getWebviewContent();
})
);
}
function getWebviewContent() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
<h1 id="lines-of-code-counter">0</h1>
<script>
const counter = document.getElementById('lines-of-code-counter');
let count = 0;
setInterval(() => {
counter.textContent = count++;
}, 100);
</script>
</body>
</html>`;
}
すごい!なんて生産的な猫だ。
Webviewスクリプトは、通常のウェブページのスクリプトができることとほぼ同じことができます。ただし、webviewsは独自のコンテキストで存在するため、webview内のスクリプトはVS Code APIにアクセスできないことに注意してください。そこでメッセージパッシングが登場します!
拡張機能からwebviewへのメッセージの受け渡し
拡張機能は、webview.postMessage()
を使用してwebviewsにデータを送信できます。このメソッドは、JSONシリアル化可能なデータをwebviewに送信します。メッセージは、標準のmessage
イベントを介してwebview内で受信されます。
これを実証するために、現在コードを書いている猫にコードをリファクタリングする(それによって総行数を減らす)よう指示する新しいコマンドをCat Codingに追加しましょう。新しいcatCoding.doRefactor
コマンドは、postMessage
を使用して現在のwebviewに指示を送信し、webview自体の中でwindow.addEventListener('message', event => { ... })
を使用してメッセージを処理します。
export function activate(context: vscode.ExtensionContext) {
// Only allow a single Cat Coder
let currentPanel: vscode.WebviewPanel | undefined = undefined;
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
if (currentPanel) {
currentPanel.reveal(vscode.ViewColumn.One);
} else {
currentPanel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{
enableScripts: true
}
);
currentPanel.webview.html = getWebviewContent();
currentPanel.onDidDispose(
() => {
currentPanel = undefined;
},
undefined,
context.subscriptions
);
}
})
);
// Our new command
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.doRefactor', () => {
if (!currentPanel) {
return;
}
// Send a message to our webview.
// You can send any JSON serializable data.
currentPanel.webview.postMessage({ command: 'refactor' });
})
);
}
function getWebviewContent() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
<h1 id="lines-of-code-counter">0</h1>
<script>
const counter = document.getElementById('lines-of-code-counter');
let count = 0;
setInterval(() => {
counter.textContent = count++;
}, 100);
// Handle the message inside the webview
window.addEventListener('message', event => {
const message = event.data; // The JSON data our extension sent
switch (message.command) {
case 'refactor':
count = Math.ceil(count * 0.5);
counter.textContent = count;
break;
}
});
</script>
</body>
</html>`;
}
Webviewから拡張機能へのメッセージの受け渡し
Webviewは拡張機能にメッセージを返すこともできます。これは、webview内の特別なVS Code APIオブジェクト上のpostMessage
関数を使用して行われます。VS Code APIオブジェクトにアクセスするには、webview内でacquireVsCodeApi
を呼び出します。この関数はセッションごとに1回しか呼び出すことができません。このメソッドから返されるVS Code APIのインスタンスを保持し、それを使用する必要がある他の関数に渡す必要があります。
Cat Coding webviewでVS Code APIとpostMessage
を使用すると、猫がコードにバグを導入したときに拡張機能に警告を出すことができます。
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{
enableScripts: true
}
);
panel.webview.html = getWebviewContent();
// Handle messages from the webview
panel.webview.onDidReceiveMessage(
message => {
switch (message.command) {
case 'alert':
vscode.window.showErrorMessage(message.text);
return;
}
},
undefined,
context.subscriptions
);
})
);
}
function getWebviewContent() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
<h1 id="lines-of-code-counter">0</h1>
<script>
(function() {
const vscode = acquireVsCodeApi();
const counter = document.getElementById('lines-of-code-counter');
let count = 0;
setInterval(() => {
counter.textContent = count++;
// Alert the extension when our cat introduces a bug
if (Math.random() < 0.001 * count) {
vscode.postMessage({
command: 'alert',
text: '🐛 on line ' + count
})
}
}, 100);
}())
</script>
</body>
</html>`;
}
セキュリティ上の理由から、VS Code APIオブジェクトはプライベートに保ち、グローバルスコープに漏洩しないようにしてください。
Web Workersの使用
Webview内ではWeb Workersがサポートされていますが、いくつかの重要な制限に注意する必要があります。
まず、Workerはdata:
またはblob:
URIを使用してのみロードできます。拡張機能のフォルダーから直接Workerをロードすることはできません。
拡張機能のJavaScriptファイルからWorkerコードをロードする必要がある場合は、fetch
を使用してみてください。
const workerSource = 'absolute/path/to/worker.js';
fetch(workerSource)
.then(result => result.blob())
.then(blob => {
const blobUrl = URL.createObjectURL(blob);
new Worker(blobUrl);
});
Workerスクリプトは、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)]
または[]
に設定してください。
コンテンツセキュリティポリシー
コンテンツセキュリティポリシーは、webviewsでロードおよび実行できるコンテンツをさらに制限します。例えば、コンテンツセキュリティポリシーは、許可されたスクリプトのリストのみがwebviewで実行されることを確実にしたり、webviewにhttps
経由でのみ画像をロードするように指示したりできます。
コンテンツセキュリティポリシーを追加するには、webviewの<head>
の先頭に<meta http-equiv="Content-Security-Policy">
ディレクティブを配置します。
function getWebviewContent() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
...
</body>
</html>`;
}
ポリシーdefault-src 'none';
はすべてのコンテンツを禁止します。その後、拡張機能が機能するために必要な最小限のコンテンツを再度有効にすることができます。以下は、ローカルスクリプトとスタイルシートのロード、およびhttps
経由での画像のロードを許可するコンテンツセキュリティポリシーです。
<meta
http-equiv="Content-Security-Policy"
content="default-src 'none'; img-src ${webview.cspSource} https:; script-src ${webview.cspSource}; style-src ${webview.cspSource};"
/>
${webview.cspSource}
の値は、webviewオブジェクト自体から来る値のプレースホルダーです。この値の使用方法の完全な例については、webviewサンプルを参照してください。
このコンテンツセキュリティポリシーは、インラインスクリプトとスタイルも暗黙的に無効にします。コンテンツセキュリティポリシーを緩和することなく、すべてのインラインスタイルとスクリプトを外部ファイルに抽出して適切にロードできるようにするのがベストプラクティスです。
https経由でのみコンテンツをロードする
webviewが外部リソースのロードを許可する場合、これらのリソースをhttp経由ではなくhttps
経由でのみロードすることを強くお勧めします。上記のコンテンツセキュリティポリシーの例では、https:
経由でのみ画像のロードを許可することで既にこれを行っています。
すべてのユーザー入力をサニタイズする
通常のウェブページと同様に、webviewのHTMLを構築する際には、すべてのユーザー入力をサニタイズする必要があります。入力を適切にサニタイズしないと、コンテンツインジェクションを許容し、ユーザーをセキュリティリスクにさらす可能性があります。
サニタイズする必要がある値の例
- ファイルの内容。
- ファイルとフォルダーのパス。
- ユーザーおよびワークスペースの設定。
HTML文字列を構築するためにヘルパーライブラリの使用を検討するか、少なくともユーザーのワークスペースからのすべてのコンテンツが適切にサニタイズされていることを確認してください。
セキュリティのためにサニタイズだけに頼らないでください。潜在的なコンテンツインジェクションの影響を最小限に抑えるために、コンテンツセキュリティポリシーを持つなど、他のセキュリティのベストプラクティスに従うことを確認してください。
永続化
標準のwebviewのライフサイクルでは、webviewはcreateWebviewPanel
によって作成され、ユーザーが閉じるか.dispose()
が呼び出されると破棄されます。しかし、webviewのコンテンツはwebviewが表示されたときに作成され、webviewがバックグラウンドに移動すると破棄されます。webviewがバックグラウンドタブに移動すると、webview内のすべての状態が失われます。
これを解決する最善の方法は、webviewをステートレスにすることです。メッセージパッシングを使用してwebviewの状態を保存し、webviewが再び表示されたときに状態を復元します。
getStateとsetState
webview内で実行されるスクリプトは、getState
およびsetState
メソッドを使用して、JSONシリアル化可能な状態オブジェクトを保存および復元できます。この状態は、webviewパネルが非表示になったときにwebviewコンテンツ自体が破棄された後も永続化されます。webviewパネルが破棄されると状態も破棄されます。
// Inside a webview script
const vscode = acquireVsCodeApi();
const counter = document.getElementById('lines-of-code-counter');
// Check if we have an old state to restore from
const previousState = vscode.getState();
let count = previousState ? previousState.count : 0;
counter.textContent = count;
setInterval(() => {
counter.textContent = count++;
// Update the saved state
vscode.setState({ count });
}, 100);
getState
とsetState
は、retainContextWhenHidden
よりもパフォーマンスオーバーヘッドがはるかに低いため、状態を永続化するための推奨される方法です。
シリアル化
WebviewPanelSerializer
を実装することで、VS Codeが再起動したときにwebviewsが自動的に復元されるようになります。シリアル化はgetState
とsetState
に基づいており、拡張機能がwebviewsのWebviewPanelSerializer
を登録した場合にのみ有効になります。
コーディング猫をVS Codeの再起動後も保持させるには、まず拡張機能のpackage.json
にonWebviewPanel
アクティベーションイベントを追加します。
"activationEvents": [
...,
"onWebviewPanel:catCoding"
]
このアクティベーションイベントは、VS CodeがviewType: catCoding
のwebviewを復元する必要があるたびに、私たちの拡張機能がアクティブ化されることを保証します。
次に、拡張機能のactivate
メソッドで、registerWebviewPanelSerializer
を呼び出して新しいWebviewPanelSerializer
を登録します。WebviewPanelSerializer
は、webviewのコンテンツをその永続化された状態から復元する責任を負います。この状態は、webviewコンテンツがsetState
を使用して設定したJSONブロブです。
export function activate(context: vscode.ExtensionContext) {
// Normal setup...
// And make sure we register a serializer for our webview type
vscode.window.registerWebviewPanelSerializer('catCoding', new CatCodingSerializer());
}
class CatCodingSerializer implements vscode.WebviewPanelSerializer {
async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel, state: any) {
// `state` is the state persisted using `setState` inside the webview
console.log(`Got state: ${state}`);
// Restore the content of our webview.
//
// Make sure we hold on to the `webviewPanel` passed in here and
// also restore any event listeners we need on it.
webviewPanel.webview.html = getWebviewContent();
}
}
これで、猫コーディングパネルを開いた状態でVS Codeを再起動すると、パネルは自動的に同じエディター位置に復元されます。
retainContextWhenHidden
非常に複雑なUIや、迅速に保存・復元できない状態を持つwebviewsの場合、代わりにretainContextWhenHidden
オプションを使用できます。このオプションは、webview自体がフォアグラウンドにない場合でも、webviewがそのコンテンツを保持し、非表示の状態を維持するようにします。
Cat Codingは複雑な状態を持つとは言い難いですが、このオプションがwebviewの動作をどのように変えるかを見るために、retainContextWhenHidden
を有効にしてみましょう。
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('catCoding.start', () => {
const panel = vscode.window.createWebviewPanel(
'catCoding',
'Cat Coding',
vscode.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true
}
);
panel.webview.html = getWebviewContent();
})
);
}
function getWebviewContent() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat Coding</title>
</head>
<body>
<img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
<h1 id="lines-of-code-counter">0</h1>
<script>
const counter = document.getElementById('lines-of-code-counter');
let count = 0;
setInterval(() => {
counter.textContent = count++;
}, 100);
</script>
</body>
</html>`;
}
webviewが非表示になり、その後復元されてもカウンターがリセットされないことに注目してください。追加のコードは不要です!retainContextWhenHidden
を使用すると、webviewはウェブブラウザのバックグラウンドタブと同様に動作します。タブがアクティブまたは表示されていない場合でも、スクリプトやその他の動的コンテンツは実行され続けます。retainContextWhenHidden
が有効になっている場合、非表示のwebviewにメッセージを送信することもできます。
retainContextWhenHidden
は魅力的かもしれませんが、メモリオーバーヘッドが高く、他の永続化手法が機能しない場合にのみ使用すべきであることを覚えておいてください。
アクセシビリティ
ユーザーがスクリーンリーダーでVS Codeを操作しているコンテキストでは、vscode-using-screen-reader
クラスがwebviewのメインボディに追加されます。さらに、ユーザーがウィンドウ内のモーション量を減らすことを希望している場合、ドキュメントのメインボディ要素にvscode-reduce-motion
クラスが追加されます。これらのクラスを監視し、それに応じてレンダリングを調整することで、webviewコンテンツはユーザーの好みをより適切に反映できます。
次のステップ
VS Codeの拡張性についてさらに詳しく知りたい場合は、以下のトピックをお試しください。