VSCode merge-diffs 插件开发

背景

当前的项目由于不是严格按照敏捷开发进行的,经常有功能分支需要分别上到 测试,预发以及开发环境。这个就导致了功能分支比较多,且开发要在临上线前整理需上线的分支,在上到正式环境之前需要合并到一个临时分支,之后再统一上到 master 分支。因此经常会遗漏掉一些分支。用 git log 可以对比出两个分支合并分支的区别,从而知道有哪些分支合并到测试分支,但是并没合并到 master。 那么这些分支就有可能是需要上线到 master 的。我个人比较习惯使用命令行 + vscode 来使用 git,但是命令行不太直观,且交互比较麻烦,因此打算写个 VSCode 插件。

关于个人项目(排除单纯为了折腾技术的目的)我有一个经验是追求的不应该是技术方案完美,而是能尽快作出需要的功能(否则极可能中途放弃),之后再不断迭代直至自己满意,因此这个过程中可能会有一些不够十全十美的地方,还望见谅。

代码地址:https://github.com/KNighD/merge-diffs

技术栈

这里我选择使用 Typescript 来实现插件,要说原因,主要是可以提示参数类型吧,毕竟不太熟悉文档。

由于存在输入时间之类的交互,因此还是需要使用到 webview 的,html 部分考虑使用 vue3 来实现(虽然react 写起来更熟悉,但是想折腾一下)

总体的想法是在页面输入查询条件,然后在与 node 端进行通信,执行命令查询出结果处理完成后回传给页面并做展示。界面 UI 的话就参考 Git History。

初始化项目

// 安装脚手架
npm install -g yo generator-code
// 生成模板代码
yo code

题外话:github 上的主分支已经改叫 main 了。。

调试

初始代码模板入口是 src/extension.ts,功能就是在命令面板输入 Hello World,将会弹出一句提示。

用 vscode 打开生成的项目,按 F5 将会弹出一个新窗口,在新窗口的命令面板输入 Hello World,如果弹出一句提示,就表明项目初始化成功了。

配置

// package.json
{
  "activationEvents": [
  		// 当命令运行时启动
      "onCommand:merge-logs.showMergeLogs"
    ],
    "contributes": {
      "commands": [
      	// 定义命令
        {
          "command": "merge-logs.showMergeLogs",
          // 命令面板上展示的
          "title": "MergeLogs: show"
        }
      ]
    }
}

打开一个 webview

let show = vscode.commands.registerCommand('merge-logs.showMergeLogs', () => {
    const panel = vscode.window.createWebviewPanel(
      'testWebview',
      // title
      'MergeLogs',
      // 显示在编辑器的哪个部位
      vscode.ViewColumn.One,
      {
        // 启用JS,默认禁用
        enableScripts: true,
        // webview被隐藏时保持状态,避免被重置
        retainContextWhenHidden: true,
      }
    )
    panel.webview.html = `<html><body>test merge logs</body></html>`
  })

  context.subscriptions.push(show)

WebView 加载的资源路径可以通过以下方法获取

const getRelativeResource = (webview: Webview, relativePath: string) => {
  return webview.asWebviewUri(Uri.file(path.join(context.extensionPath, relativePath)));
};

panel.webview.html = `<html>
<head>
<script src='${getRelativeResource(panel.webview, './test.js')}'></script>
</head>
<body>test merge logs</body>
</html>`;
});

调试的时候需要在开发窗口打开命令面板,输入 Open Webview Developer Tools

开发 web 页面

我将 web 页面放在 /client 目录下

npm init vite-app client 
cd client
npm i

项目是基于 Typescript 的,而 client 是 vue,此时根目录的 eslint 会报错,我们先将 client 目录下的文件忽略掉,将来在 client 中单独添加 eslint。

// .eslint.json
"ignorePatterns": ["/client"]

项目打包后资源文件会带上 hash 值,按照 webpack 的经验,应该可以采取两种处理方式:

  1. 修改打包配置将 public path 替换成 getRelativeResource 后获取到的地址, 动态读取打包后的 html
  2. webview 写死 html 模板,修改打包配置,让打包后的资源不带 hash,见 https://github.com/vitejs/vite/issues/378

这里采用第二种方案,在 client 下添加 vite.config.js

// vite.config.js
export default {
  rollupOutputOptions: {
    entryFileNames: `[name].js`,
    chunkFileNames: `[name].js`,
    assetFileNames: `[name].[ext]`,
  },
}

🤔不过还有一个问题,调试起来非常麻烦,每次修改完都需要 build 一下

实现简单通信

在页面上添加一个简单的按钮,点击这个按钮后,将会发一个 getLogs 的命令给插件端,插件端接收之后,执行 git log,并返回结果。

// App.vue
<template>
  <button @click="sendMessage">sendMessage</button>
</template>

<script>
export default {
  data() {
    return {
      vscode: null,
    }
  },
  mounted() {
    // 初始化 vscode 对象
    this.vscode = acquireVsCodeApi()
    // 监听来自 vscode 插件的信息
    window.addEventListener('message', (event) => {
      const message = event.data
      console.log(message)
    })
  },
  methods: {
    sendMessage() {
      // 发送消息给插件
      this.vscode.postMessage('getLogs')
    },
  },
}
</script>
// 插件端接收到消息
panel.webview.onDidReceiveMessage(
  async (message) => {
    if (message === 'getLogs') {
      try {
        const logs = await getLogs();
        panel.webview.postMessage(logs);
      } catch (error) {
        panel.webview.postMessage(error);
      }
    }
  },
  undefined,
  context.subscriptions
);
}

使用 child_process 并执行 node 命令,并封装成 Promise:

const getLogs = () => {
  return new Promise((resolve, reject) => {
    cp.exec(
      `git log`,
      (err: Error, stdout: string, stderr: string) => {
        if (stderr || err) {
          return reject(stderr || err);
        }
        return resolve(stdout);
      }
    );
  });
};

⚠️ 由于 exec 会衍生子 shell,因此在每次执行 shell 命令时要确认当前的路径是否正确。

至此一个大体的框架就完成了。

后续的开发主要是用 git branch --merged 查询分支合并情况,然后分别对比两个分支的合并分支的异同,并展示到页面上,这里不进行赘述。

添加 UI 组件库

组件库这里看到 ant-desin-vue 支持 vue3 了,打算试试。版本需要 2.0.0-beta.5 及以上。

按需引入,原先是是用 babel-plugin-import 来处理的,不过还没研究如何在 vite 中添加,就打算使用 import Button from 'ant-design-vue/lib/button'; 这样的写法,然而运行的时候,vite 会提示:

[vite] Avoid deep import "ant-design-vue/lib/button" (imported by /src/main.js)
because "ant-design-vue" has been pre-optimized by vite into a single file.
Prefer importing directly from the module entry:

import { ... } from "ant-design-vue" 

If the dependency requires deep import to function properly, 
add the deep path to optimizeDeps.include in vite.config.js.

意思是 vite 已经做了按需引入的优化了,那么应该直接导入就好了。

vue3 的 eslint 配置

填之前的坑,顺便补上 prettier 配置,最终根目录的 eslint 配置如下

{
  "root": true,
  "parser": "vue-eslint-parser",
  "parserOptions": {
    "parser": "@typescript-eslint/parser",
    "ecmaVersion": 6,
    "sourceType": "module"
  },
  "extends": ["plugin:vue/vue3-recommended"],
  "plugins": ["@typescript-eslint", "prettier"],
  "rules": {
    "@typescript-eslint/naming-convention": "warn",
    "curly": "warn",
    "eqeqeq": "warn",
    "no-throw-literal": "warn",
    "semi": "off",
    "vue/singleline-html-element-content-newline": "off"
  }
}

vite 别名

目前似乎没有支持多目录 vue 项目的 vscode 插件,见 https://github.com/vuejs/vetur/issues/424。我这里使用的是 vetur,为了避免报错,在 vite 中配置 alias。(吐槽一下:总觉得 vscode / ts 对 react 的支持还是比 vue 要好上不少的)

alias: {
  // import useInit from '/@/composables/useInit'
  '/@/': path.resolve(__dirname, './src/'),
}

发布

由于输出安装包时无需用到 client 的源码,因此可以在 .vscodeignore 中加入 client,同时修改 vite 的输出目录,以及插件中引用 client 资源的目录。

如果此前没有发布过,可以参考 https://code.visualstudio.com/api/working-with-extensions/publishing-extension#publishing-extensions。

需要在 package.json 中添加 publisher namerepository,运行 vsce package 可以打出一个 .vsix 的拓展包用于安装。

如果此前有发布过插件,那么有可能个人访问令牌已经失效了,需要到 https://aka.ms/SignupAzureDevOps 上重新创建新的个人访问令牌。

创建个人访问令牌要注意Organization要选择all accessible organizationsScopes要选择Full access,否则发布会失败。

之后添加一下 icon ,再补全一下 README 就可以发布了

vsce publish

感想

以下为个人观点:总体上感觉 Vue3 的组合式 api 和 react 的 hooks 想做的都是同一件事:将原先跟生命周期绑定在一起的逻辑抽离出来,使得代码的关注点更为清晰,同时也更易于复用,作为 hoc / mixins 的一个替代方案。

不同点是 react 是越来越偏向函数式,比如 useEffect 完全脱离了生命周期的概念,声明式编程。而 vue 依然是命令式,提供了onMounted 之类的 api。

至于 vite 还没仔细去研究,不过配置确实是少了很多,更容易上手的样子,似乎是基于 rollup ?还有待踩坑。