如何用 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

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.