これは、農工大2025アドベントカレンダーの記事です。
はじめに
VS Code 拡張機能開発において、標準の UI だけでは実現できないリッチな表現が必要な場合に Webview API を使用します。
この文書では、これの使い方についてまとめたいと思います。
Webview とは
VS Code 内に完全にカスタマイズ可能なビューを作成できる API です。
HTML、CSS、JavaScript を使用して、Web ブラウザのようなインターフェースをエディタ内に構築できます。
背景・動機
拡張機能を作っていると、「グラフを表示したい」「複雑なフォームを作りたい」「動画を埋め込みたい」といった、エディタ標準の TreeView や InputBox では対応しきれない要件が出てきます。
そんな時、自由度の高い Webview が唯一の解決策となることが多いです。
しかし、サークルのハッカソンでwebviewを使った際は意外とわかりにくく、バイブコーディングに頼ってしまったで、改めてドキュメントを読んで使い方をまとめようと思いました。
webViewの面倒なところ
Webview は上記で説明した通り、htmlやjs,cssで記述できますが、今のモダンなreactやvueのようなモダンなwebページの開発とは少し勝手が違うので注意が必要です。
メッセージパッシング
Webview (HTML側) と Extension (Node.js側) は隔離された環境で動作するため、変数の直接共有ができません。
postMessage を介した非同期なメッセージのやり取りが必要です。
リソース読み込みの制限 (CSP & URI Scheme)
普段の Web 開発のように <script src="script.js"></script> と書いても動きません。
VS Code のセキュリティポリシー (Content Security Policy) に準拠する必要があり、かつローカルファイルへのパスは vscode-resource:/ のような特殊なスキーマに変換しなければなりません。
webviewチュートリアル
公式ドキュメント:
大まかにはこれに沿ってまとめます。
プロジェクト作成
に従って作成します。
npm install --global yo generator-code
yo code
F5で実行できます。
新しく開かれたVSCodeのコマンドパレットからHello World コマンドを実行できれば完了です。
Webview パネルの作成
拡張機能のメインファイル src/extension.ts で、Webview を作成・表示するロジックを記述します。
vscode.window.createWebviewPanel を使用して、新しいパネルを作成します。
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('webviewdemo.helloWorld',()=>{
// WebviewPanelの作成
const panel = vscode.window.createWebviewPanel(
'webviewDemo', // WebviewPanelの識別子
'Webview Demo', // WebviewPanelのタイトル
vscode.ViewColumn.One, // エディタのどのカラムに表示するか
{} // Webviewのオプション
);
panel.webview.html = webviewContent();
})
)
}
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>Webview Demo</title>
</head>
<body>
<h1>Hello from Webview!</h1>
</body>
</html>
`;
}
"contributes": {
"commands": [
{
"command": "webviewdemo.helloWorld",
"title": "Hello World"
},
]
}
ご自分のプロジェクトの名前に応じでpackege.jsonを更新してコマンドを登録して下さい。
f5を押してhello worldコマンドを実行して
Hello from Webview!
が表示できれば成功です。
HTML/CSS/JS の分離と読み込み
Webviewパネル作成において、HTMLを文字列として扱ってしまうと、補完やハイライトが効かず、開発が非常に困難になります。
そこで、HTML、CSS、JSを別ファイルに分離し、読み込む方法を紹介します。
実装手順
-
HTML/CSS/JS ファイルを作成:
srcフォルダ内にwebview.html,webview.css,webview.jsを作成します -
ファイルパスの取得とURI変換: 拡張機能側 (
extension.ts) で、これらのファイルのパスを取得し、Webview 内で利用可能な URI に変換します。これにはwebview.asWebviewUriを使用します -
HTMLテンプレートの置換: HTMLファイル内のプレースホルダー(例:
{{cssUri}})を、生成した URI に置換します
src/extension.ts の実装
webviewContent 関数を作成し、ファイルの読み込みと置換を行います。
import * as path from 'path';
import * as fs from 'fs';
// ... (activate関数内など)
function webviewContent(extensionPath: string, webview: vscode.Webview) {
// 各ファイルのパスを取得
const htmlPath = path.join(extensionPath, 'src', 'webview.html');
const cssPath = vscode.Uri.file(path.join(extensionPath, 'src', 'webview.css'));
const jsPath = vscode.Uri.file(path.join(extensionPath, 'src', 'webview.js'));
// Webview用URIに変換
const cssUri = webview.asWebviewUri(cssPath);
const jsUri = webview.asWebviewUri(jsPath);
// HTMLファイルを読み込む
let html = fs.readFileSync(htmlPath, 'utf8');
// プレースホルダーを置換
html = html.replace('{{cssUri}}', cssUri.toString());
html = html.replace('{{jsUri}}', jsUri.toString());
html = html.replace(/{{cspSource}}/g, webview.cspSource);
return html;
}
src/webview.html の例
HTMLファイルでは、置換されるプレースホルダーを記述しておきます。
また、Content Security Policy (CSP) の設定も重要です。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- CSPの設定: style-src, script-src に {{cspSource}} を許可 -->
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src {{cspSource}}; script-src {{cspSource}};">
<link rel="stylesheet" href="{{cssUri}}">
<title>Webview Demo</title>
</head>
<body>
<h1>Hello from Webview!</h1>
<h1 id="messagetag">Waiting for message...</h1>
<button id="myButton">Click Me</button>
<input type="text" id="inputBox" placeholder="message to extension">
<button id="sendButton">Send to Extension</button>
<script src="{{jsUri}}"></script>
</body>
</html>
双方向通信
Webviewと拡張機能本体(Extension Host)は別プロセスで動作するため、メッセージパッシングによる通信が必要です。
Webview から Extension へ
Webview側でボタンが押されたり、入力があったりした際に、Extension側へ通知を送ります。
1. Webview側 (webview.js)
acquireVsCodeApi() を使用して API オブジェクトを取得し、postMessage でデータを送信します。
const vscode = acquireVsCodeApi();
document.getElementById('sendButton').addEventListener('click', () => {
const inputBox = document.getElementById('inputBox');
const message = inputBox.value;
// Extensionへメッセージを送信
vscode.postMessage({
command: 'inputMessage',
text: message
});
});
2. Extension側 (extension.ts)
panel.webview.onDidReceiveMessage でメッセージを受信し、処理を行います。
panel.webview.onDidReceiveMessage(
message => {
switch (message.command) {
case 'alert':
vscode.window.showErrorMessage(message.text);
return;
case 'inputMessage':
vscode.window.showInformationMessage(`Received message: ${message.text}`);
return;
}
},
undefined,
context.subscriptions
);
Extension から Webview へ
Extension側から Webview の表示を更新したい場合などに使用します。
1. Extension側 (extension.ts)
panel.webview.postMessage を使用してメッセージを送信します。
// コマンド実行時などに送信
vscode.commands.registerCommand('webviewdemo.sendMessage', () => {
if (currentPanel) {
currentPanel.webview.postMessage({ command: 'refactor' });
}
});
2. Webview側 (webview.js)
window.addEventListener('message', ...) でメッセージを受信します。
window.addEventListener('message', event => {
const message = event.data;
switch (message.command) {
case 'refactor':
const showMessage = document.getElementById('messagetag');
showMessage.textContent = 'Message received from extension!';
break;
}
});
Demo
ここまでの実装を組み合わせると、以下のようなデモが完成します。
完全なコードはここで公開しています。
-
Webviewの表示: コマンドパレットから
Webview Demo: Hello Worldを実行すると、HTMLファイルに基づいたパネルが表示されます。CSSも適用されています - Webview -> Extension: テキストボックスに文字を入力し "Send to Extension" を押すと、右下の通知エリアにその文字が表示されます
-
Extension -> Webview: コマンドパレットから
Webview Demo: Send Messageを実行すると、Webview内の "Waiting for message..." というテキストが "Message received from extension!" に変わります
終わりに
ここまでできるようになれば、自由に値を受け渡して、拡張機能を作れるでしょう!
以下ここまでわかっていれば、先人たちが詳しく解説してくれている記事が理解できると思います。
網羅的:
https://zenn.dev/ikoamu/articles/f10441bab57efc
メッセージの取り扱い:
https://qiita.com/megmogmog1965/items/aa9db6ef78d01f312733
上記の記事が個人的にわかりやすく詳しく解説してくれていると感じます
誰かの参考になれば幸いです。