VSCode 扩展 New Project 发布 0.1.0

本文最后更新于:2022年3月31日 下午

前言

vscode 扩展市场

这个 idea 起源于吾辈从 webstorm 切换到 vscode 的几周后,在上个周末,吾辈想到,为什么 Jetbrains IDE 都有 项目创建引导面板,而 vscode 却并不支持,而只能使用命令行工具呢?念及于此,吾辈便打算自行在 vscode 中实现对等插件。

吾辈之前也为 webstorm 开发了一个生成 vite 项目的插件 Vite Integrated

最终效果

new project.gif

思路

基本思路是通过 vscode webview 渲染配置,然后在主线程拼接执行 npx 命令来创建项目。具体来说,从用户的角度而言,分为以下几步

  1. 通过命令或 ui 上的菜单打开项目创建面板
  2. 选择使用的生成器
  3. 选择生成项目的位置
  4. 选择或输入一些生成器需要的配置
  5. 创建项目
  6. 如果是当前项目的子目录,打开目录中的 package.json,否则打开一个新的窗口

遇到的一些问题

  • 如何使用 react 开发视图层
  • 如何保证 webview 的界面风格与 vscode 保持一致
  • 如何实现渲染层与主线程通信
  • 使用 npm create 还是 npx
  • 如何持久化状态

如何使用 react 开发视图层

吾辈起初想将 webview 视图层独立开发部署至 github pages 上,而 vscode 插件的部分仅包含 bundle。但 vscode 默认需要使用 html 字符串作为入口,因而让事情变得有些复杂,幸好吾辈找到了官方的示例项目 hello-world-vite,这让集成吾辈熟悉的技术栈变得简单了一些,该项目便是在此模板上修改。

如何保证 webview 的界面风格与 vscode 保持一致

这个问题在早前其实会有点复杂,但随着 vscode 官方推出 ui 库 vscode-webview-ui-toolkit,并且在近期推出了默认集成 react 之后,这个问题变得简单多了 – 当然,并不意味着尽善尽美了。

官方示意图

toolkit-artwork

它提供一些基本的组件

  • badge
  • button
  • checkbox
  • data-grid
  • divider
  • dropdown
  • link
  • option
  • panels
  • progress-ring
  • radio
  • radio-group
  • tag
  • text-area
  • text-field

但是,吾辈在实际使用时仍然发现一些问题,主要包括

  • 组件的类型定义不准确,导致严重需要依赖于 storybook 的文档,梦回 JavaScript
  • 一些组件不符合直觉,例如 panels/select/text-field 和一般做法存在些许不同,panels tab/panel 要写两次,select 没有分离 value/labeltext-field 使用
  • ui 库不支持 tree shaking,导致 bundle 比较大,项目占比 439.98kb/74.79%

依赖树实在可怕,正因如此,吾辈才没有去替换 react => preact

@vscode/codicons 0.0.29
@vscode/webview-ui-toolkit 0.9.3
├── @microsoft/fast-element 1.8.0
├─┬ @microsoft/fast-foundation 2.37.2
│ ├── @microsoft/fast-element 1.8.0
│ ├── @microsoft/fast-web-utilities 5.1.0
│ ├── tabbable 5.2.1
│ └── tslib 1.14.1
├─┬ @microsoft/fast-react-wrapper 0.1.43
│ ├── @microsoft/fast-element 1.8.0
│ ├─┬ @microsoft/fast-foundation 2.37.2
│ │ ├── @microsoft/fast-element 1.8.0
│ │ ├── @microsoft/fast-web-utilities 5.1.0
│ │ ├── tabbable 5.2.1
│ │ └── tslib 1.14.1
│ └── react 17.0.2 peer
└── react 17.0.2 peer

Snipaste_2022-03-29_22-24-32

如何实现渲染层与主线程通信

由于核心是通过 postMessage/onmessage 实现,所以与 web worker 或 iframe 通信非常相似。由于代码比较简单,所以吾辈并未封装复杂的通信逻辑。

  1. 确定了通信的基础数据结构
  2. 在主线程部分使用 map 保存 channel => handle 向渲染线程暴露的功能,在接收到渲染现成的消息后执行相应的方法并通过 postMessage 扔到渲染线程
  3. 在渲染线程简单封装,让调用时传入 channel + data,即可调用到主线程暴露的方法并异步获取到结果

关键代码

主线程

const map = new Map<string, (...args: any[]) => any>();
map.set("hello", (name: string) => `hello ${name}`);
webview.onDidReceiveMessage(
  async (message: any) => {
    const { command, data = [], callback } = message;
    if (!map.has(command)) {
      throw new Error(`找不到命令 ${command}`);
    }
    const res = await map.get(command)!(...data);
    if (callback) {
      console.log("callback: ", callback);
      this._panel.webview.postMessage({
        command: callback,
        data: [res],
      });
    }
  },
  undefined,
  this._disposables
);

渲染线程

import type { WebviewApi } from "vscode-webview";

class VSCodeAPIWrapper {
  private readonly vsCodeApi: WebviewApi<unknown> | undefined;

  constructor() {
    if (typeof acquireVsCodeApi === "function") {
      this.vsCodeApi = acquireVsCodeApi();
    }
  }

  public postMessage(message: unknown) {
    if (this.vsCodeApi) {
      this.vsCodeApi.postMessage(message);
    } else {
      console.log(message);
    }
  }

  async invoke(options: {
    command: string;
    default?: any;
    args?: any[];
  }): Promise<any> {
    return await new Promise<string>((resolve) => {
      if (typeof acquireVsCodeApi !== "function") {
        resolve(options.default);
        return;
      }
      const id = Date.now() + "_" + Math.random();
      const listener = (message: MessageEvent) => {
        const data = message.data;
        if (data.command === id) {
          resolve(data.data[0]);
          window.removeEventListener("message", listener);
        }
      };
      window.addEventListener("message", listener);
      vscode.postMessage({
        command: options.command,
        data: options.args,
        callback: id,
      });
    });
  }
}

export const vscode = new VSCodeAPIWrapper();

console.log(await vscode.invoke({ command: "world" })); // hello world

使用 npm create 还是 npx

例如使用 vite 创建一个 react 项目的命令是

npx --yes --package create-vite create-vite path --template=react-ts

使用 create-react-app 创建一个项目的命令是

npx --yes --package create-react-app create-react-app path --template=typescript

可能有人会问了:“为什么没有使用类似 npm create 这个命令来创建项目呢?”
这背后确有缘由,乍看上去 create 命令已经被主流的包管理器 npm/yarn/pnpm 都支持了,但它有一些局限性

  1. npm 包名存在限制,不包含组织名的部分必须以 create- 开头,但事实上不是所有 cli 都以 create- 开头,例如 @angular/cli
  2. pnpm create 功能和 npm 不对等,需要分别适配每个包管理器
  3. webstorm 仍然在使用 npx 实现项目创建,这也影响了吾辈的选择

所以选择 npx 也便理所当然了,后续可能支持全局配置使用 pnpx

如何持久化状态

vscode 其实提供了官方的解决方案,实现 WebviewPanelSerializer 即可让 acquireVsCodeApi()getState/setState 都能被持久化,但它存在一些问题导致吾辈最终没有使用它。

  1. 仅在 webview 第一次渲染时能够恢复状态,但在没有关闭 vscode 之前多次更改都不会持久化,即如果你需要创建两个项目,第一次创建时选择了一些配置项,但第二次创建时配置项没有被缓存,而只有重开 vscode 时才会生效,这实在很奇怪(也有可能是吾辈用错了)
  2. 接口不支持 kv 存储,仅支持获取全量状态或设置全量状态

因而吾辈选择使用 context.globalState 持久化数据

主线程

// 其他代码。。。
map.set("getState", (key: string) => this.globalState.get(key));
map.set("setState", (key: string, value: any) =>
  this.globalState.update(key, value)
);

渲染线程

import type { WebviewApi } from "vscode-webview";

class VSCodeAPIWrapper {
  // 其他代码。。。
  async getState(key: string): Promise<unknown | undefined> {
    if (this.vsCodeApi) {
      return await this.invoke({ command: "getState", args: [key] });
    } else {
      const state = localStorage.getItem(key);
      return state ? JSON.parse(state) : undefined;
    }
  }

  async setState<T extends unknown | undefined>(
    key: string,
    newState: T
  ): Promise<void> {
    if (this.vsCodeApi) {
      return await this.invoke({ command: "setState", args: [key, newState] });
    } else {
      localStorage.setItem(key, JSON.stringify(newState));
    }
  }
}

export const vscode = new VSCodeAPIWrapper();

const store = (await vscode.getState(props.id)) as { location: string };

现在可以支持选择的生成器和配置项在下次使用时仍然会被缓存,这对于需要使用相同的生成器创建项目确实很方便,尤其是生成器选项比较多的情况下。

结语

vscode 目前在一些方面仍然比不上 webstorm,但其流行趋势简直势不可挡,许多前端项目甚至官方仅支持 vscode 编辑器,这让在某些时候变得没有选择(例如 vue3、nx)。所以与其抱怨,还不如尝试让它变得更好。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!