mcpField
L
lit-and-vscode-extensions

lit-and-vscode-extensions

github.com/rodydavis开发6044 次安装2 周前更新
代码仓库

关于 lit-and-vscode-extensions

这是一篇面向开发者的教程,介绍如何用 Lit Web Component 构建 VSCode 扩展,涵盖 Webview 界面、消息通信和 TypeScript 扩展命令。

主要功能

  1. 01配置 Lit 和 TypeScript 项目
  2. 02创建 Lit Web Component
  3. 03通过 Webview 消息通信
  4. 04注册扩展命令

适用场景

  1. 01构建基于 Lit 的 VSCode 扩展界面
  2. 02将 Webview 组件连接到扩展逻辑

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/rodydavis/skills --skill lit-and-vscode-extensions

安装后在 Claude Code 中输入/lit-and-vscode-extensions确认已加载。

详细介绍

Lit 和 VSCode 扩展

本文将介绍如何设置 Lit Web 组件,并使用它创建 VSCode 扩展。

TLDR 你可以在此处找到最终源码。

先决条件

  • Vscode
  • Node >= 16
  • Typescript

开始使用

我们可以先在终端中导航到项目所在位置,然后运行以下命令:

npm init @vitejs/app --template lit-ts

接着输入项目名称 lit-vscode-extension,然后在 vscode 中打开项目并安装依赖项:

cd lit-vscode-extension
npm i -D @types/node
code .

使用以下内容更新 vite.config.ts:

import { defineConfig } from "vite";
import { resolve } from "path";

export default defineConfig({
  base: "/lit-vscode-extension/",
  build: {
    outDir: "build",
    rollupOptions: {
      external: /^vscode/,
      input: {
        main: resolve(__dirname, "index.html"),
      },
      output: {
        entryFileNames: "[name].js",
      },
    },
  },
});

打开 package.json,并使用以下内容更新:

{
  "name": "lit-vscode-extension",
  "description": "Lit VSCode Extension Example",
  "version": "0.0.1",
  "publisher": "rodydavis",
  "private": true,
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/rodydavis/lit-vscode-extension"
  },
  "engines": {
    "vscode": "^1.47.0"
  },
  "categories": [
    "Other"
  ],
  "activationEvents": [
    "onCommand:lit.start",
    "onCommand:lit.reset",
    "onWebviewPanel:lit"
  ],
  "main": "./build/extension.js",
  "contributes": {
    "commands": [
      {
        "command": "lit.start",
        "title": "Open Plugin",
        "category": "lit"
      },
      {
        "command": "lit.reset",
        "title": "Reset",
        "category": "lit"
      }
    ]
  },
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "ext": "tsc src/extension.ts --outdir build --skipLibCheck --module commonjs",
    "compile": "npm run build && npm run ext",
    "serve": "vite preview"
  },
  "dependencies": {
    "lit": "^2.0.0-rc.2"
  },
  "devDependencies": {
    "@types/node": "^15.12.4",
    "@types/vscode": "^1.57.0",
    "typescript": "^4.2.3",
    "vite": "^2.3.5"
  }
}

更新 package.json 后,运行 npm i。

模板

打开 index.html,并使用以下内容更新:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Lit Example</title>
    <script type="module" src="/src/my-element.ts"></script>
  </head>
  <body>
    <my-element>
      <p>This is child content</p>
    </my-element>
  </body>
</html>

Web 组件

打开 src/my-element.ts,并使用以下内容更新:

import { html, css, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators.js";

@customElement("my-element")
export class MyElement extends LitElement {
  static styles = css`
    :host {
      display: block;
      border: solid 1px gray;
      padding: 16px;
      max-width: 800px;
    }
  `;

  @property() name = "World";
  @state() count = 0;

  render() {
    return html`
      <h1>Hello, ${this.name}!</h1>
      <button @click=${() => this.modify(1)} part="button">
        Click Count: ${this.count}
      </button>
      <slot></slot>
    `;
  }

  modify(val: number) {
    this.count += val;
  }

  reset() {
    this.count = 0;
  }

  async firstUpdated() {
    window.addEventListener(
      "message",
      (e: any) => {
        const message = e.data;
        const { command } = message;
        if (command === "reset") {
          this.reset();
        }
      },
      false
    );
  }
}

这里我们只是在示例的基础上进行修改,添加用于与 VSCode 扩展通信的消息监听器、用于更新计数的方法,以及更新渲染方法。

VSCode 通过 post 消息与插件通信,因为 UI 会在 iframe 中加载。

现在,我们来编写 src/extension.ts 中的扩展代码。首先,添加将在多处引用的顶层声明。

import * as vscode from "vscode";

const WEB_DIR: string = "build";
const WEB_SCRIPT: string = "main.js";
const TITLE: string = "Lit Example";
const TAG: string = "my-element";


const possible = [
  "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
  "abcdefghijklmnopqrstuvwxyz",
  "0123456789",
].join("");

function getNonce() {
  let text = "";
  for (let i = 0; i < 32; i++) {
    const char = possible.charAt(Math.floor(Math.random() * possible.length));
    text += char;
  }
  return text;
}

我们还创建了一个 getNonce 方法,用于确保加载的脚本正是我们传入的脚本。

现在创建一个 Panel,用于容纳插件的 UI:

class Panel {
  public static currentPanel: Panel | undefined;
  public static readonly viewType = "litExample";
  private _disposables: vscode.Disposable[] = [];

  public static createOrShow(extensionUri: vscode.Uri) {
    const column = vscode.window.activeTextEditor
      ? vscode.window.activeTextEditor.viewColumn
      : undefined;
    if (Panel.currentPanel) {
      Panel.currentPanel.panel.reveal(column);
      return;
    }
    const panel = vscode.window.createWebviewPanel(
      Panel.viewType,
      TITLE,
      column || vscode.ViewColumn.One,
      getWebviewOptions(extensionUri)
    );
    Panel.currentPanel = new Panel(panel, extensionUri);
  }

  public static revive(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
    Panel.currentPanel = new Panel(panel, extensionUri);
  }

  private constructor(
    public readonly panel: vscode.WebviewPanel,
    public readonly extensionUri: vscode.Uri
  ) {
    this._update();
    this.panel.onDidDispose(() => this.dispose(), null, this._disposables);
    this.panel.onDidChangeViewState(
      (_) => {
        if (this.panel.visible) {
          this._update();
        }
      },
      null,
      this._disposables
    );
    this.panel.webview.onDidReceiveMessage(
      (message) => {
        switch (message.command) {
          case "alert":
            vscode.window.showErrorMessage(message.text);
            return;
        }
      },
      null,
      this._disposables
    );
  }

  public sendMessage(command: string) {
    this.panel.webview.postMessage({ command: command });
  }

  public dispose() {
    Panel.currentPanel = undefined;
    this.panel.dispose();
    while (this._disposables.length) {
      const x = this._disposables.pop();
      if (x) {
        x.dispose();
      }
    }
  }

  private _update() {
    const webview = this.panel.webview;
    webview.html = this._getHtmlForWebview(webview);
  }

  private _getHtmlForWebview(webview: vscode.Webview) {
    const scriptPathOnDisk = vscode.Uri.joinPath(
      this.extensionUri,
      WEB_DIR,
      WEB_SCRIPT
    );
    const scriptUri = webview.asWebviewUri(scriptPathOnDisk);
    const nonce = getNonce();

    const slot = "<p>This is child content</p>";

    return `<!DOCTYPE html>
			<html lang="en">
        <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>${TITLE}</title>
        </head>
        <body class="vscode-light">
          <${TAG} nonce="${nonce}" >
            ${slot}
          </${TAG}>
          <script nonce="${nonce}" type="module" src="${scriptUri}"></script>
        </body>
			</html>`;
  }
}

请注意,我们重新创建了 HTML,而没有使用 index.html。这样既可以将组件用于已部署的网站,也可以用于具有独立应用逻辑代码的扩展。

其余代码都是用于管理面板状态并在完成后释放资源的样板代码。

现在,我们可以添加用于加载插件并监听命令的方法:

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand("lit.start", () => {
      Panel.createOrShow(context.extensionUri);
    })
  );

  context.subscriptions.push(
    vscode.commands.registerCommand("lit.reset", () => {
      if (Panel.currentPanel) {
        Panel.currentPanel.sendMessage("reset");
      }
    })
  );

  if (vscode.window.registerWebviewPanelSerializer) {
    vscode.window.registerWebviewPanelSerializer(Panel.viewType, {
      async deserializeWebviewPanel(
        webviewPanel: vscode.WebviewPanel,
        state: any
      ) {
        console.log(`Received state: ${state}`);
        webviewPanel.webview.options = getWebviewOptions(context.extensionUri);
        Panel.revive(webviewPanel, context.extensionUri);
      },
    });
  }
}

function getWebviewOptions(extensionUri: vscode.Uri): vscode.WebviewOptions {
  return {
    enableScripts: true,
    localResourceRoots: [vscode.Uri.joinPath(extensionUri, WEB_DIR)],
  };
}

这里我们加载扩展,并在执行 lit.reset 命令时传递消息。我们还返回 getWebviewOptions 选项,其中 enableScripts 设置为 true,以便运行注入的 js。

最终代码应如下所示:

import * as vscode from "vscode";

const WEB_DIR: string = "build";
const WEB_SCRIPT: string = "main.js";
const TITLE: string = "Lit Example";
const TAG: string = "my-element";

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand("lit.start", () => {
      Panel.createOrShow(context.extensionUri);
    })
  );

  context.subscriptions.push(
    vscode.commands.registerCommand("lit.reset", () => {
      if (Panel.currentPanel) {
        Panel.currentPanel.sendMessage("reset");
      }
    })
  );

  if (vscode.window.registerWebviewPanelSerializer) {
    vscode.window.registerWebviewPanelSerializer(Panel.viewType, {
      async deserializeWebviewPanel(
        webviewPanel: vscode.WebviewPanel,
        state: any
      ) {
        console.log(`Received state: ${state}`);
        webviewPanel.webview.options = getWebviewOptions(context.extensionUri);
        Panel.revive(webviewPanel, context.extensionUri);
      },
    });
  }
}

function getWebviewOptions(extensionUri: vscode.Uri): vscode.WebviewOptions {
  return {
    enableScripts: true,
    localResourceRoots: [vscode.Uri.joinPath(extensionUri, WEB_DIR)],
  };
}

class Panel {
  public static currentPanel: Panel | undefined;
  public static readonly viewType = "litExample";
  private _disposables: vscode.Disposable[] = [];

  public static createOrShow(extensionUri: vscode.Uri) {
    const column = vscode.window.activeTextEditor
      ? vscode.window.activeTextEditor.viewColumn
      : undefined;
    if (Panel.currentPanel) {
      Panel.currentPanel.panel.reveal(column);
      return;
    }
    const panel = vscode.window.createWebviewPanel(
      Panel.viewType,
      TITLE,
      column || vscode.ViewColumn.One,
      getWebviewOptions(extensionUri)
    );
    Panel.currentPanel = new Panel(panel, extensionUri);
  }

  public static revive(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
    Panel.currentPanel = new Panel(panel, extensionUri);
  }

  private constructor(
    public readonly panel: vscode.WebviewPanel,
    public readonly extensionUri: vscode.Uri
  ) {
    this._update();
    this.panel.onDidDispose(() => this.dispose(), null, this._disposables);
    this.panel.onDidChangeViewState(
      (_) => {
        if (this.panel.visible) {
          this._update();
        }
      },
      null,
      this._disposables
    );
    this.panel.webview.onDidReceiveMessage(
      (message) => {
        switch (message.command) {
          case "alert":
            vscode.window.showErrorMessage(message.text);
            return;
        }
      },
      null,
      this._disposables
    );
  }

  public sendMessage(command: string) {
    this.panel.webview.postMessage({ command: command });
  }

  public dispose() {
    Panel.currentPanel = undefined;
    this.panel.dispose();
    while (this._disposables.length) {
      const x = this._disposables.pop();
      if (x) {
        x.dispose();
      }
    }
  }

  private _update() {
    const webview = this.panel.webview;
    webview.html = this._getHtmlForWebview(webview);
  }

  private _getHtmlForWebview(webview: vscode.Webview) {
    const scriptPathOnDisk = vscode.Uri.joinPath(
      this.extensionUri,
      WEB_DIR,
      WEB_SCRIPT
    );
    const scriptUri = webview.asWebviewUri(scriptPathOnDisk);
    const nonce = getNonce();

    const slot = "<p>This is child content</p>";

    const htmlSource = `
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>${TITLE}</title>
      </head>
      <body class="vscode-light">
        <${TAG} nonce="${nonce}" >
          ${slot}
        </${TAG}>
        <script nonce="${nonce}" type="module" src="${scriptUri}"></script>
      </body>
    </html>`;

    return htmlSource;
  }
}

const possible = [
  "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
  "abcdefghijklmnopqrstuvwxyz",
  "0123456789",
].join("");

function getNonce() {
  let text = "";
  for (let i = 0; i < 32; i++) {
    const char = possible.charAt(Math.floor(Math.random() * possible.length));
    text += char;
  }
  return text;
}

运行

请确保运行 npm i 安装依赖项。

运行 npm run compile 编译扩展。

按 F5 打开并调试扩展。

README 配图

运行 lit: open plugin 或 lit: reset 命令来打开扩展:

README 配图

扩展打开后,在命令面板中运行 Developer: Open Webview Developer Tools 来调试扩展。

README 配图

总结

如果想进一步了解如何构建 vscode 扩展,可以阅读此处的内容;关于 Lit,则可以阅读此处的文档。

评论

支持 Markdown