刚发版就背锅?前端版本控制就靠他version-rocket

1.背景 Situation

前端后台项目发版后,最怕遇到一种情况:

用户一直开着旧页面不刷新,我们这边已经发布了新包,但他点菜单、提交表单、打开弹框的时候,仍然跑的是旧的 js。

如果只是样式改动还好,顶多页面看起来不一致。麻烦的是接口字段、路由、权限、枚举这些东西一旦变了,旧页面继续运行就容易出现一些很难复现的问题。比如:

  • 新版本接口多了字段,老页面提交参数不完整;
  • 后端已经切新逻辑,旧 js 还在按旧逻辑处理;
  • 用户说"我这里不行",但你自己打开页面又是好的;
  • 发版后让用户手动刷新,实际执行起来也不稳定。

所以这次在 Vue3 + Vite 后台项目里接入了 version-rocket,目的很简单:页面运行时自己检查远端版本,如果发现新版本,就提醒或者刷新。

2.任务 Task

这次要解决的问题不是"怎么打包",而是"已经打包发布之后,浏览器里开着的旧页面怎么知道线上有新包了"。

目标拆一下:

  1. 每次构建后生成一个版本文件,例如 version.json
  2. 页面启动后定时请求这个版本文件;
  3. 本地版本和远端版本不一致时,触发更新逻辑;
  4. 接入方式尽量轻,不要侵入业务页面。

3.行动 Action

1. 先看项目里的构建脚本

当前项目里先加了 version-rocket 依赖,然后在 scripts 里加了一个生成版本文件的命令:

对应代码大概是这样:

json 复制代码
{
  "scripts": {
    "dev": "pnpm generate:version && NODE_OPTIONS=--max-old-space-size=4096 vite",
    "generate:version": "generate-version-file dist public",
    "build": "rimraf dist && NODE_OPTIONS=--max-old-space-size=8192 vite build && pnpm generate:version",
    "build:test": "rimraf dist && vite build --mode test && pnpm generate:version"
  },
  "dependencies": {
    "version-rocket": "^1.7.4"
  }
}

这里有一个小点要注意:build 里面是先 rimraf dist,再 vite build,最后 pnpm generate:version

如果顺序反了,先生成 dist/version.json,后面 rimraf dist 或重新构建时很容易把它清掉。这样页面上线后就请求不到版本文件了。

generate-version-file dist public 会往 distpublic 都写一份 version.json。本地开发走 public,正式构建走 dist,这样调试和发版逻辑能保持一致。

当前生成出来的内容很简单:

json 复制代码
{ "version": "0.1.4", "external": "" }

默认取的是 package.json 里的 version 字段。如果后续发版不想每次手动改 package.json,也可以通过 VERSION=xxx generate-version-file dist public 这种方式从 CI 里传。

2. 在入口 App.vue 开启检测

项目里把检测放在了 src/App.vuebeforeCreate 里。这个位置比较合适,因为它足够靠近应用入口,不需要每个页面都写一遍。

实际接入重点就是这几个参数:

ts 复制代码
checkVersion({
  pollingTime: 30000,
  localPackageVersion: version,
  originVersionFileUrl: `${location.origin}${VITE_PUBLIC_PATH}version.json`,
  immediate: true,
  onVersionUpdate: (eventData: any) => {
    const newVersion = eventData.refreshPageVersion
    console.log('发现新版本:', newVersion)
    window.location.href = '/'
  },
})

说明一下:

  • localPackageVersion:当前页面运行时的版本,项目里取的是 __APP_INFO__.pkg.version
  • originVersionFileUrl:远端版本文件地址,这里拼上了 VITE_PUBLIC_PATH,避免后面项目不是根路径部署时路径写死;
  • pollingTime:当前设置为 30 秒检查一次;
  • immediate:第一次进入页面就立即检查一次,不用等第一个 30 秒;
  • onVersionUpdate:发现新版本后的处理逻辑。

当前项目里选择的是发现新版本后直接跳到 /,等于让页面重新进入一次。这个方式比较粗,但是后台系统里可接受,因为多数页面都有登录态和菜单恢复逻辑。

当然也可以不直接刷新,改成弹窗提示。代码里其实已经预留了 versionTipDialog 的方案:

ts 复制代码
versionTipDialog({
  title: '新版本',
  description: `v${newVersion} 已发布`,
  buttonText: '立即更新',
  cancelButtonText: '取消',
  cancelMode: 'ignore-current-version',
  primaryColor: '#1890ff',
  newVersion,
  needRefresh: true,
  onRefresh: () => {
    window.location.href = '/'
  },
  onCancel: () => {
    unCheckVersion({ closeDialog: true, closeWorker: true })
  },
})

我这里更倾向于先简单直接刷新,等业务反馈说"正在填表不希望被打断"的时候,再切成弹窗模式。后台项目经常会有长表单、导入、编辑态,如果后续要做得更细,弹窗方案会更友好。

3. version-rocket 内部做了什么

一开始我也想过自己写一个:

ts 复制代码
setInterval(async () => {
  const res = await fetch('/version.json?t=' + Date.now())
  const data = await res.json()
  if (data.version !== localVersion) {
    location.reload()
  }
}, 30000)

这样当然能跑,但后面要处理的细节会慢慢变多,比如重复调用会不会起多个定时器、弹窗关闭后要不要继续轮询、缓存怎么处理、以后是不是要支持指定文件检测。

version-rocket 内部的核心思路也不神秘,它是创建 worker 去请求版本文件,然后把结果回传给主线程。

关键点是:

  1. 请求 version.json 时带时间戳,减少缓存影响;
  2. worker 里拿远端 version 和本地 oldVersion 比较;
  3. 不一致时回传 refreshPageVersion
  4. 主线程拿到回调后决定是弹窗还是刷新。

这个比业务代码里散落一个 setInterval 要干净一点。以后如果要关闭检测,也可以通过 unCheckVersion 停掉 worker。

4.结果 Result

接入完成后,整个版本检测链路是这样的:

  1. package.json 维护当前版本;
  2. 构建完成后生成 dist/version.json
  3. 用户浏览器里的旧页面每 30 秒请求一次线上 version.json
  4. 如果远端版本和本地版本不一致,触发 onVersionUpdate
  5. 当前项目先直接跳首页刷新,保证重新加载最新资源。

最终效果就是:发版后不用再完全依赖用户手动刷新,旧页面会自己发现新版本。

5.项目里几个注意点

1. 版本号必须真的变化

version-rocket 通过版本号检测时,本质就是比较:

txt 复制代码
本地 package version !== 远端 version.json.version

所以如果每次发布都不改 package.json.version,或者 CI 没有注入新的 VERSION,那它就检测不到变化。

这个点很容易忘。因为代码确实发了,hash 也变了,但 version.json 里的版本号没变,页面当然认为没有新版本。

2. version.json 路径别写死

当前项目里用了:

ts 复制代码
`${location.origin}${VITE_PUBLIC_PATH}version.json`

如果项目以后不是部署在 /,而是部署在 /admin/ 之类的路径下,直接写 ${location.origin}/version.json 就可能 404。

这里跟 Vite 的 base 是一套逻辑,最好统一从 VITE_PUBLIC_PATH 取。

3. 直接刷新还是弹窗,要看业务场景

这次先用了:

ts 复制代码
window.location.href = '/'

优点是简单,发现新版本后马上回到入口,重新加载最新资源。

缺点也明显:如果用户正在编辑表单,会被打断。

如果页面里有大量编辑态、导入态、审批态,建议改成弹窗:

  • 有新版本时提示用户;
  • 用户点"立即更新"再刷新;
  • 用户点"取消"时可以忽略当前版本;
  • 复杂一点还可以结合路由,避开关键操作页面。

4. CDN 和缓存要一起看

version-rocket 请求版本文件时已经加了时间戳,但实际项目里还要看 Nginx、CDN、网关有没有额外缓存策略。

我的建议是:version.json 这种文件不要给太长缓存。它本来就是给旧页面探测新版本用的,如果它自己被缓存住,这个功能就打折了。

5. 只在需要的环境开启

checkVersion 支持 enable 参数。

如果本地开发不想轮询,可以按环境开关:

ts 复制代码
checkVersion({
  enable: MODE !== 'development',
  pollingTime: 30000,
  localPackageVersion: version,
  originVersionFileUrl: `${location.origin}${VITE_PUBLIC_PATH}version.json`,
})

当前项目开发环境也生成了 public/version.json,所以本地调试是可以跑通的。只是团队里如果觉得控制台请求太多,可以后面再收一下。

6.总结

这次接入 version-rocket,代码改动不大,主要是把"发版后旧页面怎么更新"这件事补上了。

它适合的场景也比较明确:

  • Vue / React / Vite 这类前端 SPA;
  • 后台系统发版频率比较高;
  • 用户经常长时间开着页面;
  • 不想让业务页面里到处塞版本检测逻辑。

落地时重点不在库本身,而在发版链路:

  1. 构建后一定要生成 version.json
  2. 版本号要跟着发布变化;
  3. 版本文件路径要跟部署路径一致;
  4. 更新策略要结合业务,简单系统可以直接刷新,复杂系统最好弹窗提示。

整体看下来,这个方案属于小改动解决老问题。平时不显眼,但一旦线上遇到"你那里怎么还是旧页面"这种问题,它就很有用了。

相关推荐
如果超人不会飞1 小时前
TinyVue NavMenu导航菜单组件使用指南
前端·vue.js
Jason_chen1 小时前
Linux 3.0 串口机制深度解析:传统8250驱动与基础RS-232/485支持
linux·前端
TPBoreas1 小时前
前端面试问题打靶
前端
赵庆明老师1 小时前
JS检查提交的文件是否合规
开发语言·前端·javascript
禅思院1 小时前
前端请求取消与调度完全指南:从 AbortController 到企业级优先级架构
前端·设计模式·前端框架
颂love1 小时前
Vue的两大生态以及组件通信
前端·javascript·vue.js·typescript
甜汤圆1 小时前
Python 里**自定义数据单元**
前端
cidy_982 小时前
将 Figma 接入 Codex MCP:从 `/plugins` 到本地插件配置的完整教程
前端
vivo互联网技术2 小时前
动效开发不踩坑:几种动效实现方案对比与实战选型
前端·性能优化·动效