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 的经验,应该可以采取两种处理方式:
- 修改打包配置将 public path 替换成 getRelativeResource 后获取到的地址, 动态读取打包后的 html
- 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 name 和 repository,运行 vsce package 可以打出一个 .vsix 的拓展包用于安装。
如果此前有发布过插件,那么有可能个人访问令牌已经失效了,需要到 https://aka.ms/SignupAzureDevOps 上重新创建新的个人访问令牌。
创建个人访问令牌要注意Organization要选择all accessible organizations,Scopes要选择Full access,否则发布会失败。
之后添加一下 icon ,再补全一下 README 就可以发布了
vsce publish
感想
以下为个人观点:总体上感觉 Vue3 的组合式 api 和 react 的 hooks 想做的都是同一件事:将原先跟生命周期绑定在一起的逻辑抽离出来,使得代码的关注点更为清晰,同时也更易于复用,作为 hoc / mixins 的一个替代方案。
不同点是 react 是越来越偏向函数式,比如 useEffect 完全脱离了生命周期的概念,声明式编程。而 vue 依然是命令式,提供了onMounted 之类的 api。
至于 vite 还没仔细去研究,不过配置确实是少了很多,更容易上手的样子,似乎是基于 rollup ?还有待踩坑。