如何用 vue3 开发 IDE 插件

Jul 21, 2021

简介

当你想写 IDE 插件的时候,是否觉得原生的写法太过麻烦,为什么不能用框架来写?当 webview 里的页面有十分复杂的交互的时候,写插件里的页面就变成了一件十分头疼的事,任凭外面的框架如何花里胡哨,你也只能使用原生的写法来实现,本文将介绍如何把 Vue3 运用到插件里来开发。

准备工作

创建插件

  1. 全局安装 yogenerator-code:

    npm install -g yo generator-code
    
  2. 按照步骤,新建插件,选择 Typescript 来开发插件:

    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? helloWorld
     ? What's the identifier of your extension? helloworld
     ? What's the description of your extension?
     ? Initialize a git repository? No
     ? Bundle the source code with webpack? Yes
     ? Which package manager to use?
     npm
     ❯ yarn
    

新建 vue 项目

  1. 进入插件目录,新建一个名为 frontend 的 vue 项目,需要先安装 vue cli。

    vue create frontend
    
  2. 此时,你会发现,这里的 vue 项目还是 js 的,我们把它换成 ts 的。

    ```shell
    vue add typescript
    ```
    

    注意: 其中的 Convert all .js files to .ts 选项选 Y。

新建插件要打开的 panel

新建 TemplatePanel 类

// TemplatePanel.ts
export class TemplatePanel {
  private static currentPanel: TemplatePanel | undefined;
  private readonly _panel: vscode.WebviewPanel;
  private readonly _context: vscode.ExtensionContext;
  private readonly _extensionPath: string;
  private _disposables: vscode.Disposable[] = [];

  private constructor(
    panel: vscode.WebviewPanel,
    context: vscode.ExtensionContext
  ) {
    this._panel = panel;
    this._context = context;
    this._extensionPath = context.extensionPath;
    this.initialize();
  }

  private async initialize() {
    this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
  }

  public static show(context: vscode.ExtensionContext) {
    // 如果打开了页面,不重复打开
    if (TemplatePanel.currentPanel) {
      const column = vscode.window.activeTextEditor
        ? vscode.window.activeTextEditor.viewColumn
        : undefined;
      TemplatePanel.currentPanel._panel.reveal(column);
      return;
    }

    const panel = vscode.window.createWebviewPanel(
      "Template",
      localize("template"),
      vscode.ViewColumn.Active,
      {
        enableScripts: true,
        retainContextWhenHidden: true,
        // 用于 IDE 自带的文件协议的可读路径
        localResourceRoots: [
          vscode.Uri.file(path.join(context.extensionPath, "media")),
          vscode.Uri.file(path.join(context.extensionPath, "frontend", "dist")),
        ],
      }
    );

    TemplatePanel.currentPanel = new TemplatePanel(panel, context);
  }

  public dispose() {
    TemplatePanel.currentPanel = undefined;
    this._panel.dispose();

    while (this._disposables.length) {
      const x = this._disposables.pop();
      if (x) {
        x.dispose();
      }
    }
  }
}

如何获取 vue 的前端页面

  1. 读取 vue 项目打包后的文件,并把引用的文件路径替换成 IDE 能够识别的文件协议格式。
async function getWebviewContent(extensionPath: string) {
  const distPath = vscode.Uri.file(
    path.join(extensionPath, "frontend", "dist")
  ).with({ scheme: "vscode-resource" });
  // 读取 dits 下的 index.html
  let html = await fse.readFile(
    path.join(__dirname, "../frontend/dist/index.html")
  );
  const hrefReg = /href=(["']{1})\/{1}([^\/])/gi;
  const srcReg = /src=(["']{1})\/{1}([^\/])/gi;
  // 把文件路径替换成 IDE 能够识别的文件协议格式
  let str = html
    .toString()
    .replace(hrefReg, `href=$1${distPath}/$2`)
    .replace(srcReg, `src=$1${distPath}/$2`);
  return str;
}
  1. 在 initialize 方法中赋值 webview 的 html:
// TemplatePanel.ts 中的 initialize 方法
this._panel.webview.html = await getWebviewContent(this._extensionPath);

建立插件端和前端页面(Vue)的通信

vue 编写的页面相当于是前端(浏览器端),而插件端的逻辑相当于是后端(服务器端),两者需要通信,那怎么才能在不启动服务的情况下进行通信呢,这就要用到 postMessage 了。

  1. 在前端页面中,增加 message 的监听:
// main.ts
window.addEventListener("message", (event) => {
  const message = event.data;
  switch (message.command) {
    case "setMessage": {
      break;
    }
    default:
      break;
  }
});
  1. 在插件端,也要增加 message 的监听,我们写在 initialize 方法里:
this._panel.webview.onDidReceiveMessage(
  async (message) => {
    switch (message.command) {
      case "getMessage":
        break;
      default:
        break;
    }
  },
  null,
  this._disposables
);
  1. 前端向后端发送消息:
// main.ts
// 引入 acquireVsCodeApi 的方法
const ide = acquireVsCodeApi();
// 发送消息
ide.postMessage({
  command: "getMessage",
});
  1. 后端向前端发送消息:
// templatePanel.ts
this._panel.webview.postMessage({
  command: "setMessage",
  data: {
    message: "hello",
  },
});

新增 Vuex

  1. 安装 vuex,由于我们用的是 vue3,所以要安装 vuex@next
yarn add vuex@next --save
  1. 在 store.ts 中初始化 vuex
// store.ts
import { createStore } from "vuex";

export const store = createStore({
  state: {},
  modules: {},
});
  1. 在 mian.ts 中引入 vuex
// main.ts
import { store } from "./store";
const app = createApp(App);
app.use(store);
app.mount("#app");

新增多语言(vue-i18n)

  1. 安装 vue-i18n,由于我们用的是 vue3,所以要安装 vue-i18n@next
yarn add vue-i18n@next --save
  1. 在 i18n.ts 中初始化
// i18n.ts
import { createI18n } from "vue-i18n";

const en = require("./locale/en.json");
const zhCN = require("./locale/zh-CN.json");

// 语言包根据语言环境分类
const messages = {
  en,
  "zh-CN": zhCN,
};

const i18n = createI18n({
  locale: "zh-CN", // 设置当前语言环境,默认中文简体
  messages, // 设置语言环境对应信息
});

export default i18n;
  1. 在 mian.ts 中引入 vue-i18n
// main.ts
import i18n from "./i18n/index";
const app = createApp(App);
app.use(i18n);
app.mount("#app");
  1. 可以在代码中切换语言:
this.$i18n.locale = this.language;

如何在 vue 项目中使用静态文件

由于在 IDE 的 webview 中,使用的是 IDE 的文件协议,而不是正常的 file:// 协议。当你直接使用相对路径或者绝对路径时,是找不到静态资源的,那么,要如何才能在 vue 里用上静态资源呢,方法也很简单,只需要在 vue 项目用上 IDE 的文件协议头即可,我们这里把静态资源放在 media 目录下。

  1. 获取文件协议的 uri
// 获取文件协议的 uri
this._panel.webview.asWebviewUri(
                  vscode.Uri.file(path.join(this._extensionPath, "media"))
  1. 通过之前的通信方式,把 uri 传到前端页面。
this._panel.webview.postMessage({
              command: "setSetting",
              data: {
                resource: this._panel.webview.asWebviewUri(
                  vscode.Uri.file(path.join(this._extensionPath, "media"))
                ),
              },
  1. 前端接收之后把 uri 拼接成文件协议头:
const resource =
  message.data.resource.scheme +
  "://" +
  message.data.resource.authority +
  message.data.resource.path;

在页面中使用:

<img :src="`${resource}/images/logo.png`" alt="" />

这样就可以在前端页面中使用静态资源了。

如何调试

watch vue 项目内容

在 frontend 目录下的 package.json 中配置命令:

"scripts": {
    "watch": "vue-cli-service build --watch"
  }

配置完成之后,运行 yarn watch,即可实时监听代码变化,无需重新 build。

watch 插件中内容

由于在创建插件模板的时候,已经自行创建了 watch 命令,所以只需要在项目的根目录运行 yarn watch 即可。

运用 vscode 调试

  1. 点击侧边栏的「运行和调试」,运行 Run Extension 任务。

  2. 运行之后,会打开一个新的窗口,使用快捷键 mac:cmd + shift + p win:ctrl + shift + p,输入 show panel,即可打开之前写的页面。

总结

本文介绍了如何用 vue3 来开发 IDE 插件,融合了 vuex、typescript、vue-i18n 等,能够满足日常的开发,如需查看更多的插件开发内容,可查看 文档,有丰富的 api 能够使用。

附录

项目地址 https://github.com/vivoquickapp/vue3-ide-extension-template

Tags