Webview APIを使ったVSCode拡張機能の開発方法をハンズオンっぽく解説
はじめに
先日初めてVSCodeの拡張機能を作って公開しました。
VSCodeの拡張機能の開発した際に得た知見を記事にしようと思っていたのですが、作ったものがよくある「ハンズオンで作るTodoアプリ」っぽいものになったので、雛形の作成からの実装過程をハンズオンっぽく書いていみました。
拡張機能を作ってみた経緯
日々、プロジェクトで色々なコマンドを叩いているのですが使うコマンドが多すぎて覚えられなくなってきました。
その都度READMEを確認してコマンドをコピペして実行していたのですが、コマンドを登録しておいてVSCodeのサイドバーから実行できたら便利なのでは?と思い自分用に作りました。
機能
以下のような機能を実装しました。
- Activitybar(VSCodeの左端のExploreや拡張機能のアイコンが並んでいるところ)に自分の作成した拡張機能のアイコンが表示される
- そのアイコンをクリックすると、サイドバーに登録済みのテキスト一覧と新規テキスト追加のInputが表示される
- サイドバーから登録済みテキストをクリップボードにコピー、またはターミナルに貼り付けできる
- 登録したテキストを削除できる
- テキスト登録コマンドを作成し、サイドバーを開かずに登録することができる
本記事ではこれらの機能をどう実装したかすべて解説していきます。
また、表示するWebviewを VSCodeの拡張機能っぽい 見た目にする方法も解説しています。
実装
雛形作成
https://code.visualstudio.com/api/get-started/your-first-extension に従って yeoman-generator
で雛形を作成します。
$ npm install -g yo generator-code
$ yo code
_-----_ ╭──────────────────────────╮
| | │ Welcome to the Visual │
|--(o)--| │ Studio Code Extension │
`---------´ │ generator! │
( _´U`_ ) ╰──────────────────────────╯
/___A___\ /
| ~ |
__'.___.'__
´ ` |° ´ Y `
? What type of extension do you want to create? New Extension (TypeScript)
? What's the name of your extension? my-extension
? What's the identifier of your extension? my-extension
? What's the description of your extension?
? Initialize a git repository? Yes
? Which bundler to use? unbundled
? Which package manager to use? npm
? Do you want to open the new folder with Visual Studio Code? Open with `code`
雛形が作成されたらVSCodeを開き、F5でとりあえず実行してみます。
すると、別ウィンドウで新しいVSCodeが開かれます。
新しく開かれたVSCodeのコマンドパレットからHello World
コマンドが実行できれば雛形の作成完了です。
(コマンドを実行するとウィンドウの右下にメッセージが表示されます)
サイドメニューにWebviewを表示する
次に、サイドメニューの画面を作っていきます。今回は入力フォームや登録済みのテキストなどをいい感じに表示させたいため、Tree View APIではなく、Webview APIを使ってHTML、JS、CSSをゴリゴリに書いていきます。
公式のサンプルがあるので、そちらを参考に実装していきます。
package.jsonにviewsとviewsContainersを設定する
サイドメニューにWebviewを表示させるためには、まずpackage.jsonのcontributes
に views
とviewsContainers
に諸々の設定を記述する必要があります。
また、activitybarに表示するアイコンも必要になります。
{
...
"contributes": {
"commands": [
{
"command": "my-extension.helloWorld",
"title": "Hello World"
}
],
+ "views": {
+ "my-extension-view": [
+ {
+ "type": "webview",
+ "id": "myExtension.view",
+ "name": "My Extension Name"
+ }
+ ]
+ },
+ "viewsContainers": {
+ "activitybar": [
+ {
+ "id": "my-extension-view",
+ "title": "My Extension TITLE",
+ "icon": "resources/activitybarIcon.svg"
+ }
+ ]
+ }
+ },
...
}
- activitybarのidは必ずviewsに追加したもののkey(ここでは
my-extension-view
)にする必要があります。 - activitybarのtitleはアクティブバーのアイコンをホバーした際に表示されます。
- サイドバー上部には
<activitybar.title>:<views.name>
の文字列が表示されます。- titleとnameが同じ場合は以下のように表示されます。
- titleとnameが同じ場合は以下のように表示されます。
- 今回activitybarに表示させるアイコンは、VSCodeで利用されているcodiconを使いました。
以下のサイトからダウンロードし、リポジトリ直下にresources
ディレクトリを作成し、activitybarIcon.svg
として保存します。
WebviewViewProviderを作成し、HTMLをサイドバーに表示させる
package.jsonに設定を記述したら実際にサイドバーに任意のhtmlを表示する実装をしていきます。
とりあえず適当な見出しを表示させてみます。
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
// 下で実装したWebviewViewProviderクラスのインスタンスをnewする
const provider = new MyExtensionViewProvider(context.extensionUri);
// providerをregisterWebviewViewProvider()で登録する
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(
// 👇 package.jsonに記述したviewsのidを設定する 👇
"myExtension.view",
provider,
),
);
}
class MyExtensionViewProvider implements vscode.WebviewViewProvider {
private _view?: vscode.WebviewView;
constructor(private readonly extensionUri: vscode.Uri) {}
public resolveWebviewView(webviewView: vscode.WebviewView) {
this._view = webviewView;
// ここで表示させたいHTMLを記述する
webviewView.webview.html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<h1>MY EXTENSION</h1>
</body>
</html>`;
}
}
これでHTMLが表示できました🎉
JSファイルを読み込む
次にJSファイルを読み込めるようにしてテキストの登録と登録したテキスト一覧を表示できるようにします。
まず、JSファイルを作成します。(今回は公式のサンプルに倣ってmedia/main.js
とします。)
試しにJSファイルが読み込まれたら<ul>
タグ内にテキストが挿入されるようにしてみます。
const ul = document.getElementById("list");
ul.innerText = "command list";
次に先ほど作成したMyExtensionViewProvider
でスクリプトを読み込むようにします。
class MyExtensionViewProvider implements vscode.WebviewViewProvider {
private _view?: vscode.WebviewView;
constructor(private readonly extensionUri: vscode.Uri) {}
public resolveWebviewView(webviewView: vscode.WebviewView) {
this._view = webviewView;
+ // scriptを実行するためには以下の設定が必要
+ webviewView.webview.options = {
+ enableScripts: true,
+ };
+ // 作成したJSファイルのURIを取得する
+ const scriptUri = webviewView.webview.asWebviewUri(
+ vscode.Uri.joinPath(this.extensionUri, "media", "main.js")
+ );
// ここで表示させたいHTMLを記述する
webviewView.webview.html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<h1>MY EXTENSION</h1>
+ <!-- スクリプトでテキストを挿入するulタグを追加 -->
+ <ul id="list" />
+ <!-- JSファイルを読み込む -->
+ <script src="${scriptUri}"></script>
</body>
</html>`;
}
}
テキストが挿入されました🎉
テキスト登録フォームと登録したテキスト一覧を表示する
次にInputをサイドバー上に表示し、そこから登録した値を一覧で表示できるようにしていきます。
まずはInputと登録ボタンを作ります。
class MyExtensionViewProvider implements vscode.WebviewViewProvider {
...
public resolveWebviewView(webviewView: vscode.WebviewView) {
...
webviewView.webview.html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<h1>MY EXTENSION</h1>
+ <input type="text" id="register-input" />
+ <button id="register-button">登録</button>
<!-- スクリプトでテキストを挿入するulタグを追加 -->
<ul id="list" />
<!-- JSファイルを読み込む -->
<script src="${scriptUri}"></script>
</body>
</html>`;
}
}
テキスト登録処理
jsファイルを以下のように書き換えます。
let items = [];
const registerInput = document.getElementById("register-input");
const registerButton = document.getElementById("register-button");
registerButton.addEventListener("click", () => {
const value = registerInput.value;
if (value) {
items.push(value);
registerInput.value = ""; // 登録後、Inputの値をリセットする
console.log("register", items);
}
});
「登録」ボタンを押下したらInputに入力された値をitems
に追加していきます。
登録したテキストを一覧で表示はこの後行うので、一旦登録後のitems
の状態をコンソール上にログ出力します。
テキスト一覧を表示する
次にitems
からリストを作成する関数updateItems()
を作成します。
そして、初期表示時とリストを追加した直後に実行されるようにします。
let items = [];
...
registerButton.addEventListener("click", () => {
const value = registerInput.value;
if (value) {
items.push(value);
registerInput.value = "";
- console.log("register", items);
+ // リストを更新する
+ updateItems();
}
});
+
+ function updateItems() {
+ const list = document.getElementById("list");
+ list.innerHTML = "";
+ items.forEach((item) => {
+ const li = document.createElement("li");
+ li.textContent = item;
+ list.appendChild(li);
+ });
+ }
+ // リストの初期化
+ updateItems();
これで「登録」ボタンを押下すると登録したテキストの一覧が表示されるようになりました
登録したテキストを削除できるようにする
テキストの登録・一覧表示ができたので次にテキストを削除処理を実装します。
削除ボタンを表示する
まず、<li>
タグ内にテキストと削除ボタンを表示するようにします。
function updateItems() {
const list = document.getElementById("list");
list.innerHTML = "";
items.forEach((item) => {
const li = document.createElement("li");
+ const deleteButton = document.createElement("button");
+ deleteButton.textContent = "削除";
+ li.appendChild(document.createTextNode(item));
+ li.appendChild(deleteButton);
- li.textContent = item;
list.appendChild(li);
});
}
...
(見栄えが終わっていますが一旦このまま進めていきます。)
削除ボタン押下でテキストが削除されるようにする
削除ボタンを押下した時のイベントをイベントリスナーに追加します。
今回はクリックしたテキストと一致する要素をitems
から除外するようにしています。
(そのため、同じテキストが複数ある場合はそれら全部が消えてしまうようになっています)
function updateItems() {
const list = document.getElementById("list");
list.innerHTML = "";
items.forEach((item) => {
const li = document.createElement("li");
const deleteButton = document.createElement("button");
+ // 削除ボタンがクリックされたときの処理
+ deleteButton.addEventListener("click", () => {
+ items = items.filter((i) => i !== item);
+ updateItems(); // リストを更新する
+ });
deleteButton.textContent = "削除";
li.appendChild(document.createTextNode(item));
li.appendChild(deleteButton);
li.textContent = item;
list.appendChild(li);
});
}
...
登録済みテキストをクリップボードにコピー・ターミナルにペーストできるようにする
削除ボタンと同様に<li>
タグ内に「クリップボードにコピー」ボタン・「ターミナルにペースト」ボタンを追加し、2つのボタンのクリックイベントをリスナーに追加します。
+ const vscode = acquireVsCodeApi();
let items = [];
...
function updateItems() {
const list = document.getElementById("list");
list.innerHTML = "";
items.forEach((item) => {
const li = document.createElement("li");
const deleteButton = document.createElement("button");
// 削除ボタンがクリックされたときの処理
deleteButton.addEventListener("click", () => {
items = items.filter((i) => i !== item);
updateItems(); // リストを更新する
});
deleteButton.textContent = "削除";
+ // クリップボードにコピーボタン
+ const copyToClipboardButton = document.createElement("button");
+ copyToClipboardButton.textContent = "コピー";
+ copyToClipboardButton.addEventListener("click", () => {
+ // 💡拡張機能側にメッセージを送信
+ vscode.postMessage({
+ type: "copyToClipboard",
+ text: item,
+ });
+ });
+ // ターミナルにペーストボタン
+ const pasteToTerminalButton = document.createElement("button");
+ pasteToTerminalButton.textContent = "ペースト";
+ pasteToTerminalButton.addEventListener("click", () => {
+ // 💡拡張機能側にメッセージを送信
+ vscode.postMessage({
+ type: "pasteToTerminal",
+ text: item,
+ });
+ });
li.appendChild(document.createTextNode(item));
li.appendChild(deleteButton);
+ li.appendChild(copyToClipboardButton);
+ li.appendChild(pasteToTerminalButton);
list.appendChild(li);
});
}
この2つのボタンをクリックした時の処理はWebview側(media/main.js
)ではなく、 VSCode拡張側(src/extension.ts
)で行う必要があります。
そのため、ボタンをクリックした際にvscode.postMessage()
でWebviewから拡張機能へメッセージを受け渡し、MyExtensionViewProvider
でコピー処理やターミナルへの貼り付け処理を実施します。
class MyExtensionViewProvider implements vscode.WebviewViewProvider {
private _view?: vscode.WebviewView;
...
public resolveWebviewView(webviewView: vscode.WebviewView) {
this._view = webviewView;
...
// ここで表示させたいHTMLを記述する
webviewView.webview.html = `...`;
+ // Webviewからメッセージを受け取った時の処理
+ webviewView.webview.onDidReceiveMessage((data) => {
+ switch (data.type) {
+ case "copyToClipboard":
+ // クリップボードにコピーする処理
+ break;
+ case "pasteToTerminal":
+ // テキストをターミナルに貼り付ける処理
+ break;
+ }
+ });
+ }
}
クリップボードにコピー
MyExtensionViewProvider
にテキストをクリップボードにコピーするメソッドを追加し、onDidReceiveMessage
で data.type
が copyToClipboard
の場合にそのメソッドを呼び出すようにします。
クリップボードへのコピーはvscode.env.clipboard.writeText()
を使います。
class MyExtensionViewProvider implements vscode.WebviewViewProvider {
private _view?: vscode.WebviewView;
...
public resolveWebviewView(webviewView: vscode.WebviewView) {
this._view = webviewView;
...
// ここで表示させたいHTMLを記述する
webviewView.webview.html = `...`;
// Webviewからメッセージを受け取った時の処理
webviewView.webview.onDidReceiveMessage((data) => {
switch (data.type) {
case "copyToClipboard":
- // クリップボードにコピーする処理
+ this._copyToClipboard(data.text);
break;
case "pasteToTerminal":
// テキストをターミナルに貼り付ける処理
break;
}
});
}
+ private _copyToClipboard(text: string) {
+ // クリップボードにコピー
+ vscode.env.clipboard.writeText(text);
+ // コピーしたことをメッセージで表示する
+ vscode.window.showInformationMessage(`📋Copied to clipboard \"${text}\"`);
+ }
}
ターミナルにペースト
クリップボードにコピーと同様にメソッドを作成し、onDidReceiveMessage
に記載します。
ターミナルにテキストを入力するには terminal.sendText()
を使います。
テキストを入力する対象のターミナルは以下のように決定するようにします。
-
vscode.window.activeTerminal()
でアクティブなターミナルが取得できる場合- そのターミナルにテキストを入力する
-
vscode.window.activeTerminal()
でアクティブなターミナルが取得できなかった場合-
vscode.window.terminals
でターミナルが取得できた場合はvscode.window.terminals[0]
にテキストを入力する -
vscode.window.terminals
でターミナルが取得できなかった場合はvscode.window.createTerminal()
で新規のターミナルを開き、そこにテキストを入力する
-
class MyExtensionViewProvider implements vscode.WebviewViewProvider {
private _view?: vscode.WebviewView;
...
public resolveWebviewView(webviewView: vscode.WebviewView) {
this._view = webviewView;
...
// ここで表示させたいHTMLを記述する
webviewView.webview.html = `...`;
// Webviewからメッセージを受け取った時の処理
webviewView.webview.onDidReceiveMessage((data) => {
switch (data.type) {
case "copyToClipboard":
this._copyToClipboard(data.text);
break;
case "pasteToTerminal":
- // テキストをターミナルに貼り付ける処理
+ this._pasteToTerminal(data.text);
break;
}
});
}
...
+ private _pasteToTerminal(text: string) {
+ const paste = (terminal: vscode.Terminal) => {
+ /// 入力したテキストを即時に実行しないために、shouldExecuteはfalseにしている
+ terminal.sendText(text, false);
+ // terminalにフォーカスする
+ terminal.show();
+ };
+
+ // 現在アクティブなターミナルを取得する
+ const activeTerminal = vscode.window.activeTerminal;
+ if (activeTerminal) {
+ paste(activeTerminal);
+ return;
+ }
+
+ // VSCodeで開いているターミナル一覧を取得する
+ const terminals = vscode.window.terminals;
+ if (terminals.length) {
+ paste(terminals[0]);
+ } else {
+ // 新規のターミナルを作成する
+ const newTerminal = vscode.window.createTerminal();
+ paste(newTerminal);
+ }
+ }
}
登録コマンドを作成する
サイドバーのInputからだけではなく、VSCodeのInputBoxを使ってクリップボードに保存されている値を瞬時に登録できるコマンドを作成します。
package.jsonにcommandsを設定する
package.jsonにviews
とviewsContainers
を設定するのようにpackage.jsonに登録コマンドを設定します。
{
...
"contributes": {
"commands": [
+ {
+ "command": "my-extension.addItemFromInputBox",
+ "title": "Add Item from InputBox"
+ }
- {
- "command": "my-extension.helloWorld",
- "title": "Hello World"
- }
],
(ついでに雛形で作成されたHello World
コマンドを削除しています。)
コマンド実行時の処理を拡張機能に実装する
export function activate(context: vscode.ExtensionContext) {
const provider = new ItemsViewProvider(context.extensionUri);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(
ItemsViewProvider.viewType,
provider,
),
);
+ // コマンドを登録する
+ context.subscriptions.push(
+ vscode.commands.registerCommand(
+ // package.jsonに設定した`commands.command`を設定する
+ "my-extension.addItemFromInputBox",
+ () => {
+ provider.addItemFromInputBox();
+ }
+ ),
+ );
}
export class ItemsViewProvider implements vscode.WebviewViewProvider {
private _view?: vscode.WebviewView;
public static readonly viewType = "sideClipboard.itemsView";
constructor(private readonly extensionUri: vscode.Uri) {}
+ public addItemFromInputBox() {
+ // 現在クリップボードに保存されている値を取得し、InputBoxの初期値とする
+ vscode.env.clipboard.readText().then((text) => {
+ if (!this._view) {
+ return;
+ }
+ const input = vscode.window.createInputBox();
+ input.placeholder = "New Item";
+ input.value = text;
+ // InputBoxでEnterが押された時のイベント登録
+ input.onDidAccept((_) => {
+ if (!input.value) {
+ return;
+ }
+ // Webview側にメッセージを渡す
+ this._view?.webview.postMessage({
+ type: "addItem",
+ text: input.value,
+ });
+ // InputBoxを閉じる
+ input.hide();
+ });
+
+ // InputBoxを表示する
+ input.show();
+ });
+ }
public resolveWebviewView(webviewView: vscode.WebviewView) {
...
}
}
コピー・ターミナルへペースト機能とは逆に、拡張側からWebviewにメッセージを渡します。
...
function updateItems() { ... }
+ window.addEventListener("message", (event) => {
+ const message = event.data;
+ switch (message.type) {
+ case "addItem": {
+ items.push(message.text);
+ updateItems();
+ break;
+ }
+ }
+ });
// リストの初期化
updateItems();
登録したテキストを永続化する
VSCodeが閉じられても登録したテキストがなくならないように items
の値を永続化させます。
main.js読み込み時にvscode.getState()
で永続化した値を取得し、items
を変更するたびにvscode.setState()
で永続化している値を更新します。
const vscode = acquireVsCodeApi();
+ // 永続化されたstateを取得
+ const previousState = vscode.getState();
+ let items = previousState?.items ?? [];
- let items = [];
...
function updateItems() {
+ // itemsが更新されたタイミングで値を保存する
+ vscode.setState({ items });
const list = document.getElementById("list");
list.innerHTML = "";
items.forEach((item) => {
...
});
}
レイアウトを整える
値の永続化までができたので、次にCSSで見た目を整えていきます。
CSSファイルを読み込む
JSファイルを読み込む同様に、media/main.css
を作成し、resolveWebviewView
内のHTMLでcssファイルを読み込むようにします。
試しにheaderの文字色を赤にしてみます。
h1 {
color: red;
}
class MyExtensionViewProvider implements vscode.WebviewViewProvider {
private _view?: vscode.WebviewView;
constructor(private readonly extensionUri: vscode.Uri) {}
...
public resolveWebviewView(webviewView: vscode.WebviewView) {
this._view = webviewView;
// scriptを実行するためには以下の設定が必要
webviewView.webview.options = {
enableScripts: true,
};
// 作成したJSファイルのURIを取得する
const scriptUri = webviewView.webview.asWebviewUri(
vscode.Uri.joinPath(this.extensionUri, "media", "main.js")
);
+ // 作成したCSSファイルのURIを取得する
+ const styleUri = webviewView.webview.asWebviewUri(
+ vscode.Uri.joinPath(this.extensionUri, "media", "main.css")
+ );
// ここで表示させたいHTMLを記述する
webviewView.webview.html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <!-- CSSファイルを読み込む -->
+ <link rel="stylesheet" href="${styleUri}">
</head>
<body>
<h1>MY EXTENSION</h1>
<input type="text" id="register-input" />
<button id="register-button">登録</button>
<!-- スクリプトでテキストを挿入するulタグを追加 -->
<ul id="list" />
<!-- JSファイルを読み込む -->
<script src="${scriptUri}"></script>
</body>
</html>`;
...
}
VSCodeのテーマカラーを利用する
WebviewはCSSカスタムプロパティを使ってVSCodeのテーマカラーを参照することができます。
これにより、ユーザーがどんなThemeを使っていたとしても、自然なデザインになってくれます。
ウェブビューはCSS変数を使ってVS Codeのテーマカラーにアクセスすることもできます。 これらの変数名は先頭に
vscode
を付け、.
を-
に置き換えます。 例えばeditor.foreground
はvar(--vscode-editor-foreground)
となります。
使用可能なテーマ変数については、テーマカラーリファレンスを参照してください。 変数のインテリセンス候補を提供する拡張機能が利用できます。
ここの色は何を使っているんだろう?という時は開発者ツールから変数名を確認しながら実装しました。
.register-form-container {
display: flex;
}
.register-input {
border: 1px solid var(--vscode-input-border);
border-radius: 2px;
background-color: var(--vscode-editor-background);
color: var(--vscode-editor-foreground);
width: 100%;
}
.register-button {
background-color: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: 1px solid var(--vscode-button-border);
border-radius: 2px;
margin-left: 10px;
padding: 4px 10px;
width: 100px;
}
class MyExtensionViewProvider implements vscode.WebviewViewProvider {
...
public resolveWebviewView(webviewView: vscode.WebviewView) {
...
// ここで表示させたいHTMLを記述する
webviewView.webview.html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- CSSファイルを読み込む -->
<link rel="stylesheet" href="${styleUri}">
</head>
<body>
<h1>MY EXTENSION</h1>
+ <div class="register-form-container">
+ <input type="text" id="register-input" class="register-input" />
+ <button id="register-button" class="register-button">登録</button>
+ </div>
- <input type="text" id="register-input" />
- <button id="register-button">登録</button>
<!-- スクリプトでテキストを挿入するulタグを追加 -->
<ul id="list" class="registered-list" />
<!-- JSファイルを読み込む -->
<script src="${scriptUri}"></script>
</body>
</html>`;
...
}
...
}
codiconを使う
次に、Webview上にVSCode上で使われているcodiconを表示します。
今回は例として登録ボタンの「登録」というテキストの部分をcodiconのadd(+)アイコンにしてみます。
まず@vscode/codiconsをnpm等でインストールします。
$ npm i @vscode/codicons
次に、()[]と同様にインストールしたcodiconのcssをWebviewのHTMLに読み込みます。
class MyExtensionViewProvider implements vscode.WebviewViewProvider {
private _view?: vscode.WebviewView;
constructor(private readonly extensionUri: vscode.Uri) {}
...
public resolveWebviewView(webviewView: vscode.WebviewView) {
this._view = webviewView;
// scriptを実行するためには以下の設定が必要
webviewView.webview.options = {
enableScripts: true,
};
// 作成したJSファイルのURIを取得する
const scriptUri = webviewView.webview.asWebviewUri(
vscode.Uri.joinPath(this.extensionUri, "media", "main.js")
);
// 作成したCSSファイルのURIを取得する
const styleUri = webviewView.webview.asWebviewUri(
vscode.Uri.joinPath(this.extensionUri, "media", "main.css")
);
+ // インストールした`@vscode/codicons`のCSSファイルのURIを取得する
+ const codiconsUri = webviewView.webview.asWebviewUri(
+ vscode.Uri.joinPath(
+ this.extensionUri,
+ "node_modules",
+ "@vscode/codicons",
+ "dist",
+ "codicon.css",
+ ),
+ );
// ここで表示させたいHTMLを記述する
webviewView.webview.html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- CSSファイルを読み込む -->
<link rel="stylesheet" href="${styleUri}">
+ <!-- codiconのCSSファイルを読み込む -->
+ <link rel="stylesheet" href="${codiconsUri}">
</head>
<body>
<h1>MY EXTENSION</h1>
<input type="text" id="register-input" />
<button id="register-button">登録</button>
<!-- スクリプトでテキストを挿入するulタグを追加 -->
<ul id="list" />
<!-- JSファイルを読み込む -->
<script src="${scriptUri}"></script>
</body>
</html>`;
...
}
アイコンは<i>
タグのclassにcodicon
とcodicon-XXX
(XXX部分はアイコン名)を設定すると表示されます。
webviewView.webview.html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- CSSファイルを読み込む -->
<link rel="stylesheet" href="${styleUri}">
<!-- codiconのCSSファイルを読み込む -->
<link rel="stylesheet" href="${codiconsUri}">
</head>
<body>
<h1>MY EXTENSION</h1>
<input type="text" id="register-input" />
+ <button id="register-button" class="register-button">
+ <span style="vertical-align:middle">登録</span>
+ <!-- codiconのaddアイコンを表示 -->
+ <i style="vertical-align:middle" class="codicon codicon-add"></i>
+ </button>
- <button id="register-button">登録</button>
<!-- スクリプトでテキストを挿入するulタグを追加 -->
<ul id="list" />
<!-- JSファイルを読み込む -->
<script src="${scriptUri}"></script>
</body>
</html>`;
これでレイアウトの実装は完了です🎉
リソースのアクセスを制御する
機能・レイアウトの実装が完了したので、最後にリソースのアクセスを制御します。
localResourceRootsでローカルリソースへのアクセスを制御する
webviewView.webview.options
の localResourceRoots
を設定することで、webviewがロードするリソースを制限することができます。
今回の例では以下2つのパスを指定します。
- 自前で実装したjsファイル(
main.js
)とcssファイル(main.css
)が配置されているmedia
ディレクトリ - npm等で
node_modules
にインストールされた@vscode/codicons/dist
ディレクトリ
class MyExtensionViewProvider implements vscode.WebviewViewProvider {
private _view?: vscode.WebviewView;
constructor(private readonly extensionUri: vscode.Uri) {}
...
public resolveWebviewView(webviewView: vscode.WebviewView) {
this._view = webviewView;
webviewView.webview.options = {
enableScripts: true,
+ localResourceRoots: [
+ vscode.Uri.joinPath(this.extensionUri, "media"),
+ vscode.Uri.joinPath(
+ this.extensionUri,
+ "node_modules",
+ "@vscode/codicons",
+ "dist",
+ ),
+ ],
};
// 作成したJSファイルのURIを取得する
const scriptUri = webviewView.webview.asWebviewUri(
vscode.Uri.joinPath(this.extensionUri, "media", "main.js")
);
// 作成したCSSファイルのURIを取得する
const styleUri = webviewView.webview.asWebviewUri(
vscode.Uri.joinPath(this.extensionUri, "media", "main.css")
);
// ここで表示させたいHTMLを記述する
webviewView.webview.html = `...`
}
}
Content Security Policy(CSP)を指定する
今回の拡張機能では特に必要なさそうですが、公式Exampleに倣ってCSPを記述し、読み込まれるリソースを制限します。
<script>
に設定するnonce値は公式に従ってランダムな文字列を生成するgetNonce
関数を作成してその値を設定します。
class MyExtensionViewProvider implements vscode.WebviewViewProvider {
private _view?: vscode.WebviewView;
constructor(private readonly extensionUri: vscode.Uri) {}
...
public resolveWebviewView(webviewView: vscode.WebviewView) {
...
+ const nonce = getNonce();
// ここで表示させたいHTMLを記述する
webviewView.webview.html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webviewView.webview.cspSource}; style-src ${webviewView.webview.cspSource}; script-src 'nonce-${nonce}';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- CSSファイルを読み込む -->
<link rel="stylesheet" href="${styleUri}">
<!-- codiconのCSSファイルを読み込む -->
<link rel="stylesheet" href="${codiconsUri}">
</head>
<body>
<h1>MY EXTENSION</h1>
<input type="text" id="register-input" />
<button id="register-button">登録</button>
<!-- スクリプトでテキストを挿入するulタグを追加 -->
<ul id="list" />
<!-- JSファイルを読み込む -->
+ <script nonce="${nonce}" src="${scriptUri}"></script>
- <script src="${scriptUri}"></script>
</body>
</html>`;
...
}
+ function getNonce() {
+ let text = "";
+ const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ for (let i = 0; i < 32; i++) {
+ text += possible.charAt(Math.floor(Math.random() * possible.length));
+ }
+ return text;
+ }
これで本記事で説明する内容は以上です。お疲れ様でした!🎉
さいごに
VSCode拡張はドキュメントのボリュームがあり、公式のサンプルもかなりたくさんあるので逆にどこから手をつけていいかわからなくなっていたので、同じように困っている人の手助けになれば幸いです。
また、公開した拡張機能は今後もアップデートしていくつもりなので、使ってみて実装してほしい追加機能等あればissueを出していただけるととっても喜びます。(今今はWebviewをReactで開発できないかを模索中。。。💭)
Discussion