1.背景 Situation
前端后台项目发版后,最怕遇到一种情况:
用户一直开着旧页面不刷新,我们这边已经发布了新包,但他点菜单、提交表单、打开弹框的时候,仍然跑的是旧的 js。
如果只是样式改动还好,顶多页面看起来不一致。麻烦的是接口字段、路由、权限、枚举这些东西一旦变了,旧页面继续运行就容易出现一些很难复现的问题。比如:
- 新版本接口多了字段,老页面提交参数不完整;
- 后端已经切新逻辑,旧 js 还在按旧逻辑处理;
- 用户说"我这里不行",但你自己打开页面又是好的;
- 发版后让用户手动刷新,实际执行起来也不稳定。
所以这次在 Vue3 + Vite 后台项目里接入了 version-rocket,目的很简单:页面运行时自己检查远端版本,如果发现新版本,就提醒或者刷新。
2.任务 Task
这次要解决的问题不是"怎么打包",而是"已经打包发布之后,浏览器里开着的旧页面怎么知道线上有新包了"。
目标拆一下:
- 每次构建后生成一个版本文件,例如
version.json; - 页面启动后定时请求这个版本文件;
- 本地版本和远端版本不一致时,触发更新逻辑;
- 接入方式尽量轻,不要侵入业务页面。
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 会往 dist 和 public 都写一份 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.vue 的 beforeCreate 里。这个位置比较合适,因为它足够靠近应用入口,不需要每个页面都写一遍。

实际接入重点就是这几个参数:
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 去请求版本文件,然后把结果回传给主线程。

关键点是:
- 请求
version.json时带时间戳,减少缓存影响; - worker 里拿远端
version和本地oldVersion比较; - 不一致时回传
refreshPageVersion; - 主线程拿到回调后决定是弹窗还是刷新。
这个比业务代码里散落一个 setInterval 要干净一点。以后如果要关闭检测,也可以通过 unCheckVersion 停掉 worker。
4.结果 Result
接入完成后,整个版本检测链路是这样的:
package.json维护当前版本;- 构建完成后生成
dist/version.json; - 用户浏览器里的旧页面每 30 秒请求一次线上
version.json; - 如果远端版本和本地版本不一致,触发
onVersionUpdate; - 当前项目先直接跳首页刷新,保证重新加载最新资源。
最终效果就是:发版后不用再完全依赖用户手动刷新,旧页面会自己发现新版本。
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;
- 后台系统发版频率比较高;
- 用户经常长时间开着页面;
- 不想让业务页面里到处塞版本检测逻辑。
落地时重点不在库本身,而在发版链路:
- 构建后一定要生成
version.json; - 版本号要跟着发布变化;
- 版本文件路径要跟部署路径一致;
- 更新策略要结合业务,简单系统可以直接刷新,复杂系统最好弹窗提示。
整体看下来,这个方案属于小改动解决老问题。平时不显眼,但一旦线上遇到"你那里怎么还是旧页面"这种问题,它就很有用了。