5.8 KiB
web-admin 发版后强制刷新
日期:2026-10-16
适用范围:web-admin生产构建与运行时
交互参考:docs/superpowers/demos/2026-10-16-web-admin-version-refresh-demo.html(方案 A · 强制弹窗)
决策:静态version.json+ 强制弹窗,确认后刷新页面
1. 目标与已确认决策
1.1 目标
- 管理端发版后,仍停留在旧标签页的用户能得知已有新版本。
- 用户只能通过「立即刷新」进入新包,不能关掉提示继续用旧功能。
- 不依赖后端接口;随静态资源部署即生效。
1.2 决策摘要
| 项 | 约定 |
|---|---|
| 方案 | A:Vite 构建写出 dist/version.json,前端比对 buildId |
| 交互 | 强制弹窗,无「稍后 / 取消 / 关闭」;确认后 location.reload() |
| 未保存表单 | 不另做二次确认,直接弹刷新提示 |
| 展示版本 | 现有 VITE_VERSION(注入 __APP_VERSION__) |
| 比对指纹 | buildId = 短 git hash + 构建时间戳(注入 __APP_BUILD_ID__) |
| 轮询 | 生产环境启动后立即查一次,之后每 10 分钟 |
| 回前台 | document.visibilityState === 'visible' 时立刻再查;距上次请求不足 30 秒则跳过 |
| 开发环境 | pnpm dev 不检查 |
1.3 非目标
- 后端版本接口、按发版类型切换「可稍后」
- 小程序更新(已有
miniprogram/utils/mpUpdate.js) - 未保存表单的草稿保护或刷新前二次确认
- 修改
VITE_VERSION的人工发版流程(展示号可不变,buildId每次构建都会变)
2. 背景
web-admin 是 Vite 打包的 SPA(hash 路由)。用户长时间不关标签页时,内存里仍是旧的 JS。现有 __APP_VERSION__(来自 VITE_VERSION,当前 3.0.2)只用于本地存储键隔离(web-admin/src/utils/storage/storage-config.ts),发版后不会提示刷新。若发版时忘记改 VITE_VERSION,仅比对该字段还会漏报。因此展示用版本号,是否更新用每次构建都变化的 buildId。
3. 构建产物
3.1 dist/version.json
构建结束时写入,与 index.html 同目录(受 VITE_BASE_URL 影响时,文件仍在 outDir 根上,由静态服务器按 base 提供):
{
"version": "3.0.2",
"buildId": "a1b2c3d-1730000000000"
}
| 字段 | 来源 | 用途 |
|---|---|---|
version |
VITE_VERSION |
弹窗展示 |
buildId |
git rev-parse --short HEAD,失败时用 nogit;再拼接 - + 构建时刻毫秒时间戳 |
是否为新包的唯一比对键 |
同一 commit 再次 pnpm build,时间戳不同,buildId 必须变化。
3.2 包内常量
在现有 define.__APP_VERSION__ 旁增加 __APP_BUILD_ID__,值与当次 version.json 的 buildId 相同。类型声明写在 web-admin/src/env.d.ts。
3.3 插件位置
在 web-admin/vite.config.ts 的 plugins 中增加一个仅 apply: 'build' 的插件:closeBundle(或等价的构建结束钩子)写文件,并在 config 阶段把 buildId 配进 define。开发服务器不写该文件、不注入会触发检查的生产逻辑。
4. 运行时
4.1 模块
新增 web-admin/src/utils/check-app-update.ts,由 web-admin/src/main.ts 在 app.mount 之后调用 startAppUpdateCheck()。
职责:
import.meta.env.PROD为假时直接返回。- 请求
${import.meta.env.BASE_URL}version.json?t=${Date.now()},cache: 'no-store'。 - 解析 JSON。
buildId为非空字符串且与__APP_BUILD_ID__不同时弹窗。 - 弹窗已打开或已经弹过则不再弹。弹出时清除定时器并移除
visibilitychange监听,不再继续检查。
4.2 调度
- 启动后立即检查一次。
setInterval10 分钟。visibilitychange:变为visible时检查;若距上次发起请求不足 30 秒则跳过。- 页面卸载时移除监听并清除定时器。
4.3 弹窗
使用 Element Plus ElMessageBox.alert(自动导入,不手写 import):
| 项 | 值 |
|---|---|
| 标题 | 发现新版本 |
| 正文 | 系统已发布更新,当前页面仍在使用旧功能。请刷新后继续操作。 |
| 版本行 | 远端 version 为非空字符串时追加:当前 v{__APP_VERSION__} / 最新 v{远端 version} |
| 确认按钮 | 立即刷新 |
showClose |
false |
closeOnClickModal |
false |
closeOnPressEscape |
false |
showCancelButton |
false |
用户点击「立即刷新」后执行一次 location.reload()(模块内标志位保证只触发一次)。遮罩无背景模糊,与现有弹窗一致。
多个标签页各自检查、各自弹窗。
5. 失败处理
| 情况 | 行为 |
|---|---|
网络失败、非 2xx、JSON 无法解析、缺少 buildId |
不提示,保留定时器,下一轮再试 |
buildId 相同 |
不提示 |
buildId 不同 |
弹一次窗并停止后续检查 |
远端无 version |
仍按 buildId 弹窗,正文不带版本行 |
6. 部署注意
前端请求已绕过 HTTP 缓存。静态服务器仍应为 index.html 与 version.json 设置短缓存或 no-cache。二者若被长期强缓存,用户点刷新后仍可能拿到旧的 index.html。带 hash 的 JS/CSS 可继续长期缓存。
本设计不新增 Nginx 配置文件;上线时由现有发布方式保证这两份入口文件可被重新验证。
7. 验证
- 连续构建两次,两次
dist/version.json的buildId不同。 - 用第一版打开页面,再换成第二版静态资源:等待轮询,或切走标签页超过 30 秒后再切回,出现不可关闭的「发现新版本」。
- 点击「立即刷新」后加载第二版,不再弹窗。
- 删除或无法访问
version.json时,业务页面仍可操作,控制台不把该失败当成未捕获异常打断应用。 pnpm dev不请求version.json。