はじめに
Webview は新しい扉を開きました:
The webview API allows extensions to create fully customizable views within Visual Studio Code. Webviews can also be used to build complex user interfaces beyond what VS Code's native APIs support.
VS Code プラグインが HTML をレンダリングして複雑な UI を作成できるようにし、API がサポートするものに限定されません。この柔軟性により、プラグインに多くの可能性が生まれました:
This freedom makes webviews incredibly powerful, and opens up a whole new range of extension possibilities.
一.vscode.previewHtml コマンド
初期は vscode.previewHtml コマンドを使用して HTML コンテンツをレンダリングしていました:
// Render the html of the resource in an editor view.
vscode.commands.executeCommand(
'vscode.previewHtml',
uri,
viewColumn,
title
);
本質的にはiframe です(詳細は html preview part and command を参照)。組み込みの Markdown プレビューなどの機能をサポートするために使用されました
後にセキュリティと互換性の面で問題が発生しました:
However the vscode.previewHtml command suffered from some important security and compatibility issues that we determined could not be fixed without breaking existing users of the command.
そのため、Webview API に置き換えることになりました:
The webview API is significantly easier to work with, correctly supports different filesystem setups, and webviews also offer many security benefits over htmlPreviews.
二.Webview API
previewHtml と比較して、Webview はより安全ですが、リソース消費も大きい です:
Webviews are resource heavy and run in a separate context from normal extensions.
その実行環境は Electron のネイティブ Webview タグ です。iframe と比較して、最大の違いは Webview が独立したプロセスで実行され、セキュリティ分離性がより強いことです:
Unlike an iframe, the webview runs in a separate process than your app. It doesn't have the same permissions as your web page and all interactions between your app and embedded content will be asynchronous. This keeps your app safe from the embedded content.
一方、Webview の使用にはパフォーマンス負荷があるため、公式は再三にわたり「術高莫用」を強調しています:
Webviews are pretty amazing, but they should also be used sparingly and only when VS Code's native API is inadequate.
また、Webview を使用する前に 3 点を考慮することを推奨しています:
-
この機能は本当に VS Code 内に配置する必要がありますか?独立したアプリケーションまたはウェブサイトとしての方が適切ではありませんか?
-
Webview は目標機能を実現する唯一の方法ですか?通常のプラグイン API で置き換えることはできませんか?
-
創造できるユーザー価値は Webview が消費するリソースに見合っていますか?
三.具体的な使い方
具体的には、vscode.window.createWebviewPanel を使用して Webview を作成します:
// 1. 作成して Webview を表示
const panel = vscode.window.createWebviewPanel(
// 該 webview の識別子、任意の文字列
'catCoding',
// webview パネルのタイトル、ユーザーに表示される
'Cat Coding',
// webview パネルが所在するカラム
vscode.ViewColumn.One,
// その他の webview オプション
{}
);
P.S. Webview パネルは作成後、webview.title を使用してタブページタイトルを変更できます
次に webview.html を使用して Webview 内でレンダリングする HTML コンテンツを設定します:
// 2. webview がレンダリングする HTML コンテンツを設定
panel.webview.html = `<!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>`;
vscode.previewHtml と同様、指定された HTML コンテンツは最終的にiframe を通じてロードされます。ただし、このiframe は Webview によってレンダリングされます。したがって、之前的な方式と比較して、セキュリティ問題を解決するための Webview 環境が 1 層追加されただけです
ライフサイクル
Webview パネルは作成後、2 つの重要なライフサイクルイベントがあります:
-
隠す/回復する:
onDidChangeViewState。可視性(webview.visible)が変化した時、および Webview が異なるカラムにドラッグアンドドロップされた時(panel.viewColumn)にトリガーされます。通常、状態の保存/回復に使用されます -
破棄:
onDidDispose。パネルが閉じられた時にトリガーされ、timer を停止するなどのクリーンアップ作業を完了するために使用されます
特に、Webview がバックグラウンドに入るとコンテンツは破棄され、再度可視化時にこれらのコンテンツが再作成されます:
The contents of webviews however are created when the webview becomes visible and destroyed when the webview is moved into the background. Any state inside the webview will be lost when the webview is moved to a background tab.
例えば、ユーザーがタブを切り替えた後、Webview が表示しているコンテンツは破棄され、ランタイム状態もクリアされます。ユーザーが切り戻るか、またはプラグインがpanel.reveal() を使用して Webview をユーザーの眼前に戻すと、Webview コンテンツが再ロードされます。ユーザーによって閉じられるか、またはプラグインがpanel.dispose() を使用して閉じると、Webview およびそのコンテンツはすべて破棄されます
状態の保存と回復
そのため、Webview は状態を保持するメカニズムを提供しています:
// webview
vscode.getState({ ... })
vscode.setState({ ... })
Webview コンテンツの回復に使用できます。例えば:
// webview
const vscode = acquireVsCodeApi();
const counter = document.getElementById('lines-of-code-counter');
// 之前に保存した状態値を取り出す
const previousState = vscode.getState();
let count = previousState ? previousState.count : 0;
counter.textContent = count;
setInterval(() => {
counter.textContent = count++;
// 状態値が更新されたら書き戻す
vscode.setState({ count });
}, 100);
P.S. ここで、acquireVsCodeApi は Webview 環境に注入されるグローバル関数で、VS Code が提供するgetState などの API にアクセスするために使用されます
注意が必要なのは、setState() を通じて保存された状態はWebview パネルが閉じられる時に破棄されます(永続化保存されません):
The state is destroyed when the webview panel is destroyed.
永続化して保持したい場合は、WebviewPanelSerializer インターフェースを実装する必要があります:
// package.json
// 1. package.json で onWebviewPanel:viewType プラグインアクティベーション方式を宣言
"activationEvents": [
...,
"onWebviewPanel:catCoding"
]
// extension.ts
// 2. WebviewPanelSerializer インターフェースを実装
vscode.window.registerWebviewPanelSerializer('catCoding',
new class implements vscode.WebviewPanelSerializer {
async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel, state: any) {
// Webview コンテンツを回復、state は webview 中で setState によって保存された状態
webviewPanel.webview.html = restoreMyWebview(state);
}
}
);
このようにすれば、VS Code は再起動後に自動的に Webview コンテンツを回復できます
手動での保存回復以外に、もう 1 つの簡単な方法はretainContextWhenHidden オプションを設定することです(createWebviewPanel 時にパラメータとして渡す)。Webview が不可視の時でもコンテンツを保持するよう要求します(サスペンドに相当)。しかし、大きなパフォーマンスオーバーヘッドをもたらすため、このオプションの使用は慎重に 推奨します
通信
Webview コンテンツは隔離された環境で実行されますが、VS Code はプラグインと Webview の間でメッセージメカニズムを提供しており、双方向通信 を実現できます:
// プラグイン 送信
webview.postMessage({ ... })
// webview 受信
window.addEventListener('message', event => { ... })
// webview 送信
const vscode = acquireVsCodeApi()
vscode.postMessage({ ... })
// プラグイン 受信
webview.onDidReceiveMessage(
message => { ... },
undefined,
context.subscriptions
);
したがって、Webview 状態の保存と回復は完全に手動で実装できます。setState() などの API が要件を満たせない場合に対応できます
テーマ適応
JS を注入して追加 API を提供する以外に、VS Code はスタイル適応をサポートするために、いくつかの class および CSS 変数を事前設定しています
例えば、body には 3 つの事前設定された class 値があります:
-
vscode-light:ライトテーマ -
vscode-dark:ダークテーマ -
vscode-high-contrast:ハイコントラストテーマ
これら 3 つの状態を使用してテーマ適応を完了できます。例えば:
body.vscode-light {
color: black;
}
body.vscode-dark {
color: white;
}
body.vscode-high-contrast {
color: red;
}
また、ユーザーが設定した具体的な色値も CSS 変数を通じて公開されています:
--vscode-editor-foreground は editor.foreground に対応
--vscode-editor-font-size は editor.fontSize に対応
四.デバッグ
Webview は独立した環境で実行されるため、DevTools を通じて直接デバッグできません。そのため、VS Code は 2 つのコマンドを提供しています:
-
Developer: Open Webview Developer Tools:現在可視の Webview の DevTools を開く -
Developer: Reload Webview:すべての Webview をリロードし、その内部状態をリセットし、ローカルリソースを再読み込み
Webview 向けの DevTools は、Toggle Developer Tools コマンドで開く DevTools が VS Code 自身の UI をデバッグするのと同様に、Webview コンテンツをデバッグできます
Webview コンテンツがローカルリソースをロードしている場合、Reload Webview コマンドを使用して再ロードできます。プラグインを再起動したり Webview を再オープンしたりする必要はありません
五.セキュリティ制限
以前のvscode.previewHtml コマンドでも、現在の Webview API でも、大量のセキュリティ制限 が存在します:
-
Webview 内ではジャンプがサポートされていません。
aタグをクリックしても反応しません。プラグインを通じて Webview コンテンツを修正して迂回的にジャンプを実現することを推奨します -
それでも
iframe環境に制限されます(iframeが Webview 内に配置されただけ)。例えば、X-Frame-Options: SAMEORIGIN設定を含むレスポンスヘッダーを持つページはロードできません(詳細は #76384、#70339 を参照) -
Electron
webviewタグのいくつかのセキュリティオプションが開放されていません。例えばallow-modalsにより、alertができません(詳細は #67109) -
ローカルリソースのロードが制限されています。デフォルトではプラグインディレクトリおよび開いているワークスペースディレクトリへのアクセスのみが許可されており、特定の API(
webview.asWebviewUri)を通じて変換するか、または<base href="${mediaPath}">タグを通じてローカルリソースのルートパスを設定する必要があります(詳細は #47631)
例えば、同一生成元ポリシーにより、いくつかのリソースをiframe 通じてロードできません:
Refused to display 'https://code.visualstudio.com/api/extension-guides/webview' in a frame because it set 'X-Frame-Options' to 'sameorigin'.
此类のエラーは直接キャッチできません(詳細は Catch error if iframe src fails to load)。しかし、iframe 通じてリソースをロードする前に、そのリソースへのアクセスを試み、アクセス可能であることを確認してからロードできます:
fetch(url).then(() => {
// iframe 通じてロード可能
frames[0].src = url;
}, () => {
// iframe 通じてロード不可、提示する
});
六.まとめ
一見柔軟で開放的だが実際には制限が極めて多い です。現在(2019/12/14)、VS Code の Webview 能力の位置付けは HTML レンダラーに過ぎず、UI 拡張能力の補完です:
You should think of the webview more as an html view (one that does not have any server or origin) rather than a webpage.
(#72900 より抜粋。Webview API 作者自身の述べています)
コメントはまだありません