公司的私有vscode插件,如何自动更新(附带思路)

4,452 阅读7分钟

图片来源: github.com/Microsoft/v…

震惊, 10多万start的仓库6年前没解决的问题, 居然被一个刚毕业者解决了?

好吧我承认我标题党了(但只是子标题), 我并不是从根源解决了这个问题, 而是用一种临时的方案去达到相近的效果,等vscode支持了私有插件市场后, 我的这个方案将不值一提。

上图描述的问题是vscode不支持私有插件市场, 但实际上用户需要的可能只是插件私有化和自动更新。下图的discussion维护者也说明了这个目的。

image.png

图片来源: github.com/microsoft/v…

我的方法是基于npm私有仓库的,公司没有npm私有仓库的话就借鉴一下我的思路自己搞一下吧, 用远程文件上传下载+版本号管理的方式也是可以搞的

嫌啰嗦想直接使用的, 可以直接看 总结用法

追溯痛点

vscode插件的更新机制

vscode有个默认的插件更新机制:打开vscode窗口时,vscode会去检测所有插件的版本,如果有最新版本就会自动更新。

但是只有发布到vscode插件市场上的插件才能享受自动更新机制,如果是你们公司的插件,且老板不允许你们将插件市场发布到插件市场上呢,那该怎么自动更新?

谈谈现状

google上大肆搜索vscode插件更新 私有插件等关键词,也只能看到:每次更新都需要将插件重新打包成vsix文件,然后下发给团队的各个成员,让他们手动安装。

这很煞笔,不是吗,如果插件更新频繁的话,手动行为就会非常频繁,这明显很不符合程序员的作风。

这时候应该会有人想到:npm仓库都能搭建私服,那vscode插件市场能搭建私服吗?

可惜不支持,甚至连检测插件和更新插件的接口都无从获取,这就没办法去在公司内网去代理接口修改数据的行为。

我的方案

核心思路

将vsix文件以npm包的方式存储到npm私有仓库上,因为npm仓库提供了两个功能:

  1. 版本管理:可以轻松的发布新的版本,同时可以通过网络api去检测版本
  2. 文件io服务:npm仓库提供了发布和下载vsix文件的途径

但有个问题是, 检测更新逻辑是写在代码里的, 插件启动后, 才会去执行代码。所以虽然能起到自动更新的效果, 但也比不上vscode检测更新的行为那么丝滑。这也是没有的办法, 因为vscode貌似没暴露出vscode启动前的钩子。

细节设计

我已经封装好一个基于npm仓库进行插件自动更新的包了,你们可以自行下载来使用: www.npmjs.com/package/upd…

update-vscode-extension 需要在你的vscode插件中使用, 支持功能有: 定时检测版本、更新插件等

1. 打包附带visx文件的npm包

vsix文件本身作为npm包进行更新和发布, npm包结构如下

/ # 包的根目录
  ./extension.vsix # 通过vsce命令打包的vsix文件
  ./package.json

这里vsix文件是vscode插件打包后的产物, 打包方式是通过vscecli执行: vsce package

然后把该npm包上传到你们团队的npm私有仓库上

2. 检测版本

这里是利用npm仓库服务的api去检测更新的, 不做过多讲解,这里我也封装了一个npm包,直接拿来即用 —— npm-pkg-version

检测到最新版本后,则需要去更新插件了

3. 更新插件

vscode安装vsix文件的核心api是 vscode-root-path/bin/code --install-extension vsix-file-path

先将携带vsix文件的npm包下载到本地的某个暂存目录中, 然后调用核心api去安装它。

插件自动更新机制到这里就结束了。

一开始我以为已经结束了,然而...

4. 最后一个痛点

一般来说, npm私有仓库的包名是必须按 @scope/pkg-name 的格式的, 而vscode的插件名是不能带 @scope 的, 这就导致了插件源码的package.json不能和npm包的package.json共用一个, 所以生成npm包就会费劲许多。

这里解释一下为什么vscode的插件名是不能带 @scope 的:

        其实vscode插件名是能带 @scope 的, 只不过不推荐而已, 但是用 vsce 打包时如果package.json的name包含 @scope 就会报错。

这里提供两个个有解决思路:

思路1: 写一套发布新版本的脚本(不推荐)。

  1. 包名保持为 scope-pkg-name 的格式, 用 vsce 打包vsix文件
  2. 将package.json的name转成 @scope/pkg-name
  3. 更新package.json的version
  4. 发布npm包
  5. 发布后将package.json的name再转成 scope-pkg-name

这样就能保证vsix携带的包名是 符合vscode插件名规范的(scope-pkg-name), 且发布到npm私有仓库上的包名也是符合 @scope/pkg-name 的格式的。

这个打包-发布的方案的最大缺点就是不可中断不可控, 整个流程如果中间执行一半就意外结束了,就会很麻烦。

思路2: 比如npm包名直接保持为 scope-pkg-name 格式, 然后基于 vsce package 再去封装一个cli —— update-vscode-extension-cli 简称 uvec (推荐)。

uvec: www.npmjs.com/package/uve…

uvec 的目的是打包目标npm包并发布, 这样就能与源码隔离开来, 不会有一丝的副作用了。

uvec支持两种模式,默认模式: 打包并发布npm包;onlyBuild模式: 只构建npm包, 剩余流程靠你自己实现, 更详细的可以进链接自己看看。

总结用法

  1. 准备工作

假设你已经有个现成的vscode插件了,如果没有,可以用 yo generator-code 创建一个。

然后安装依赖包:

npm i update-vscode-extension -S

npm i uvec -D

  1. 编写自动更新逻辑

update-vscode-extension 支持对检测更新的自动、手动、暂停、继续等操作。

这里是自动更新的写法, 直接在插件入口 src/extension.ts 下注册即可:

import vscode, { ExtensionContext } from 'vscode';
import registerUpdateVscodeExtension from 'update-vscode-extension';
import packageJSON from '../package.json';

const registerUpdate = async () => {  
  const { runSlice } = registerUpdateVscodeExtension(packageJSON.name, {
    currentVersion: packageJSON.version,
    vscodeAppRoot: vscode.env.appRoot,
    interval: 10 * 60 * 1000, // 十分钟检测更新一次
  });

  await runSlice();
};

export async function activate(context: ExtensionContext) {
    registerUpdate();
    
    // you code
}

或者你想要支持手动更新、并可以支持配置间隔, 也可以实现, 具体的不多说, 大致看看就行:

import { isHupuOnline } from '@/utils/utils';
import vscode, { window } from 'vscode';
import registerUpdateVscodeExtension from 'update-vscode-extension';
import packageJSON from '../package.json';

let checkAndUpdate: (() => Promise<void>) | null = null;
let singletonStop: (() => void) | null = null;

const defaultInterval = 10 * 60 * 1000;
const minInterval = 60 * 1000;

const releaseNPMName = '@hupu/vscode-extension';

const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 0);

const restartAutoUpdate = (interval: number | null) => {
  singletonStop?.();

  if (interval !== null && interval < minInterval) {
    window.showInformationMessage(`自动更新间隔设置得太小: ${interval}; 已帮你自动设置成: ${defaultInterval}`);
    interval = defaultInterval;
  }

  const { runSlice, stop } = registerUpdateVscodeExtension(releaseNPMName, {
    npmTag: 'latest',
    registryUrl: 'http://hnpm.hupu.io/',
    currentVersion: packageJSON.version,
    vscodeAppRoot: vscode.env.appRoot,
    interval,
    async beforeCheck() {
      if (!await isHupuOnline({ timeout: 5000, checkUrl: 'http://hnpm.hupu.io' })) {
        throw new Error('连不上<http://hnpm.hupu.io>,自动更新插件停止');
      }
    },
    async beforeUpdate(err) {
      if (err) {
        const errMsg = err.toString().includes('404') ? `npm包[${releaseNPMName}]未找到` : err.toString();
        vscode.window.showErrorMessage(`检测插件[键盘侠]最新版本失败: ${errMsg}`);
      } else {
        statusBarItem.text = '插件[键盘侠]自动更新中...';
        statusBarItem.show();
      }
    },
    async afterUpdate(err) {
      statusBarItem.hide();
      if (err) {
        vscode.window.showErrorMessage(`插件自动更新失败: ${err}`);
      } else {
        const buttonLabel = await vscode.window.showInformationMessage(
          `插件[键盘侠]自动更新完毕, 是否重启该窗口`,
          '是',
          '否',
        );
        if (buttonLabel === '是') {
          vscode.commands.executeCommand('workbench.action.reloadWindow');
        }
      }
    },
  });

  checkAndUpdate = runSlice;
  singletonStop = stop;

  runSlice();
};

const registerUpdate = async (ctx: vscode.ExtensionContext) => {
  restartAutoUpdate(null);

  ctx.subscriptions.push(
    statusBarItem,
    vscode.commands.registerCommand('hupu.check-version-and-update'.checkVersionAndUpdate, async () => {
      await checkAndUpdate?.();
    }),
    vscode.commands.registerCommand('hupu.restart-auto-update', async () => {
      const interval = await window.showInputBox({
        title: '设置检查更新的间隔',
        placeHolder: '请设置检查更新的间隔 (单位ms)',
        value: (config.autoUpdateInterval && config.autoUpdateInterval > minInterval) ? `${config.autoUpdateInterval}` : `${defaultInterval}`,
        validateInput(value) {
          if (!value.match(/^\d+$/)) {
            return '必须为数值';
          }
          if (+value < minInterval) {
            return '间隔太小容易卡死...';
          }
          return null;
        },
      });
      if (interval) {
        config.autoUpdateInterval = +interval;
        restartAutoUpdate(+interval);
        window.showInformationMessage('自动更新已开启/重启');
      }
    }),
    vscode.commands.registerCommand('hupu.close-auto-update', async () => {
      singletonStop?.();
      window.showInformationMessage('自动更新已关闭');
    }),
  );
};

export default registerUpdate;

  1. 发布新版本

在package.json中的scripts加入update

{
  "scripts": {
    "update": "uvec package . --registry-url='http://hnpm.hupu.io/' --pkg-name='@hupu/vscode-extension' --vsce.no-yarn --vsce.allow-star-activation"
  }
}

这里的 @hupu/vscode-extension 记得替换成你最终要发布到npm私有仓库上的包名, 另外如果需要加一些vsce运行时的参数,可以通过 --vece.param=xxx 的方式进行配置。更详细的可以看 www.npmjs.com/package/uve…

如果想更新最新版本, 运行 npm run update 就行了。

然后结合之前在vscode中写的自动更新逻辑, 你的vscode插件就可以自动更新了。

  1. 编写 README

先用 vsce package 命令打包个vsix文件, 然后上传到cdn上, 让团队的所有人都安装一次, 后面就全靠自动更新了, 不需要再手动安装。

## 下载与安装

[点击下载](https://static.hoopchina.com.cn/upload-xxx.vsix)

安装方式: <https://xxxxxxxx>

安装后在vscode中插件会自动更新到最新版本

为自己打个广告

这是我的github首页: github.com/z-juln

造了很多我觉得很有意义的轮子, 比如:

这些npm包我应该算是首创轮子吧?反正到现在没找到相似的npm包

在虎扑实习加工作时长有1年多了, 个人觉得技术还是很ok的, 明年7月左右打算去厦门找份少加班的前端工作(主要是离家近)。

简历: github首页应该能算半个简历吧?

微信: A1850021148