2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Webview を使った VSCode Extension

Last updated at Posted at 2024-11-18

本記事の目的

表題の通り、Webview を用いた VSCode Extension の作り方について書いていますが、

VSCode Extension の作り方については以下の公式ドキュメントを読めば分かります。

また、Webview についてもきちんと解説があります。

本記事はほぼ自分用のメモでして、作ってみた結果いくつか注意点等ありましたので、コードをテンプレ化してみた感じです。

テンプレコード

本記事のコードは↑にあります。次の手順でデバッグ実行できます。

  1. VSCode 上で ⌘ + Shift + P を押してコマンドパレットを開きます

  2. パレットから Debug: Start Debugging を選択します. すると新しい Extension 実行用の VSCode が起動します

  3. Extension 実行用の VSCode 上で ⌘ + Shift + P を押してコマンドパレットを開きます

  4. パレットから example-vscode-extension-webview: Hello World を選択します

ディレクトリ構造

ほぼ公式チュートリアルの構造と同じですが、一部都合に併せて改変しています。

.
├── out
│   └── extension.js
│
├── src
│   ├── command
│   │   ├── _app.ts
│   │   ├── _utils.ts
│   │   └── *.ts
│   └── extension.ts
│
├── media
│   ├── cat.gif
│   ├── events.js
│   └── style.css
│
├── eslint.config.mjs
├── tsconfig.json
└── package.json
Directory 説明
out/* npm run compile すると、ここに tsc コンパイルの結果が出力されます。
src/extensions.ts VSCode Extension のエントリコードです。
src/command/* webview ⇔ Extension 間の複雑性を縮減するために型化したコードです。 webview 側から呼べる API の定義をしています。
media/* src/extension.ts コードで定義した Webview (html) で参照しているリソース類の置き場です。
e.g. <link>, <script>, <img>...

注意・工夫点

tsconfig.json

tsconfig.json
  {
    "compilerOptions": {
-     "module": "Node16",  // or "CommonJS"
+     "module": "CommonJS",  
      "target": "ES2022",
      "outDir": "out",
      "lib": [
        "ES2022"
      ],
      "sourceMap": true,
      "rootDir": "src",
      "strict": true,   /* enable all strict type-checking options */
      /* Additional Checks */
      // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
      // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
      // "noUnusedParameters": true,  /* Report errors on unused parameters. */
+     "esModuleInterop": true
    }
  }

大事な点ですが、VSCode Extension 自体は package.json / tsconfig.json の設定で CommonJS プロジェクトとして設定する必要があります。

2024/11 現在、プロジェクトを ESM として設定する と動きません!

これを踏まえて tsconfig.json"module": "..." には2つの選択肢があります。

module 説明
Node16 VSCode Extension の標準はこれで今後一般的に推奨されます。

しかし一部の npm modules (e.g. @langchain) では tsc コンパイル時に後述するエラーが発生してしまいます。
CommonJS 昔からの挙動をとる互換のための設定で、最近では Node16 に置き換えられつつあります。

がしかし前述の Node16 の問題は、こちらを設定すると解消されます。現時点では VSCode Extension は CommonJS module しか参照できないので特に問題はない筈...

NPM で公開されている一部の commonjs / ESM 両対応の module では package.json"exports": の設定誤りによって tsc コンパイル時に次のエラーが発生します。

error TS1479: The current file is a CommonJS module whose imports will produce 'require' calls;
  however, the referenced file is an ECMAScript module and cannot be imported with 'require'.

また、"module": "CommonJS" を選択する際は、明示的に "esModuleInterop": true を設定する必要があります。

これを設定しないと import fs from 'fs' の様な default import でコンパイルエラーが発生してしまいます。

"module": "Node16" の場合は default = true となるので、VSCode Extension の初期コードでは消されているようです。

eslint.config.mjs

VSCode Extension の default コードは古い書き方になっているため、2024/11 現在で推奨されている flat config & typescript-eslint のものに書き換えています。

src/extension.ts

このファイルが VSCode Extension のエントリコードです。
Extension の初期化・登録と、Webview html の生成を行っています。

ここではチュートリアルコードのまま、コード中に文字リテラルとして Webview html を直接記述しています。

リソース類 (.gif, .css, .js) は後述する media/* ディレクトリにおいてあり、src= 属性でパス参照をします。

html ヘッダでは <meta http-equiv="Content-Security-Policy" ... /> を設定しており、javascript インラインコード等は禁止しています。

src/extension.ts
import * as vscode from 'vscode'
import { commandRouter } from './command/_app'

export function activate(context: vscode.ExtensionContext) {
  // ...省略...
}

function getWebviewContent(cspSource: string, gif: vscode.Uri, script: vscode.Uri, css: vscode.Uri) {
  return `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta
      http-equiv="Content-Security-Policy"
      content="default-src 'none'; img-src ${cspSource} https:; script-src ${cspSource}; style-src ${cspSource};"
    />
    <title>Cat Coding</title>
    <link rel="stylesheet" href="${css.toString()}">
</head>
<body>
    <img src="${gif.toString()}" width="300" />

    <button id="findfiles">List files</button>
    <textarea id="files"></textarea>

    <button id="createfile">Create &quot;hello.txt&quot;</button>

    <script src="${script.toString()}"></script>
</body>
</html>`
}

src/command/*.ts

こちらは完全に独自のファイルです。 Webview (html) から Extension 宛に呼べる API の定義をしています。

tRPC / zod のインターフェースを真似てみました。

_app.ts
import { z } from 'zod'
import { createCommandRouter, CommandFactory } from './_utils'
import type { SendResponse, Command } from './_utils'
import { findFiles } from './findfiles'
import { createFile } from './createfile'

export const commandRouter = createCommandRouter(getHandlers)

function getHandlers(sendResponse: SendResponse): Command[] {
  return [
    // 'findfiles' API 定義.
    CommandFactory
      .command('findfiles')
      .input(z.object({
        dir: z.string(),
      }))
      .query(async (message) => {  // .input(zod) で定義した型の message が来る.
        // process.
        const files = await findFiles(message)

        // send response.
        sendResponse({
          command: message.command,
          result: files,
        })
      }),

    // 'createfile' API 定義.
    CommandFactory
      .command('createfile')
      .input(z.object({}))
      .query(async (message) => {
        await createFile(message)
      }),
  ]
}

※ Webview 本来の仕組み

VSCode Extension では、Webview (html) からは vscode.postMessage() で、逆に Extension からは webview.postMessage() でメッセージ送信できます。

設計として一貫性を持たせないと、コードが複雑化し簡単に循環参照に陥りそうなので本 API 定義コードを書きました。

media/*

html から参照する公開リソースの置き場です。

公式チュートリアルに従い media/* としていますが、他フレームワークではよく public/* と命名されることが多いですね。

ここで特に重要なのは media/event.js で、このファイルは Webview (html) に <script src="..."></script> 埋め込みで参照されています。

この javascript ファイルでは主に Webview (html) の DOM Event 処理をします。

1. メッセージ送信 (Webview → Extension)

Webview (html) 上のボタンが押された際の処理をコーディングしています。
vscode.postMessage() で Extension 側で定義した API を呼んでいます。

media/event.js
const vscode = acquireVsCodeApi()  // eslint-disable-line no-undef

const buttonFindfiles = document.getElementById('findfiles')
const buttonCreatefile = document.getElementById('createfile')

// send findfiles command.
buttonFindfiles.addEventListener('click', () => {
  vscode.postMessage({
    command: 'findfiles',
    params: {
      dir: '.',
    },
  })
})

// send createfile command.
buttonCreatefile.addEventListener('click', () => {
  vscode.postMessage({
    command: 'createfile',
    params: {},
  })
})

2. メッセージ受信 (Extension → Webview)

逆にこちらのコードでは Extension 側からのメッセージ応答を受信しています。

media/event.js
const textarea = document.getElementById('files')

// Handle the message inside the webview
window.addEventListener('message', event => {
  const message = event.data

  // define event handlers.
  const eventHandlers = {
    findfiles: () => {
      textarea.value = message.result.join('\n')
    },
  }

  // call handler.
  eventHandlers[message.command]()
})
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?