# 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. 用户只能通过「立即刷新」进入新包,不能关掉提示继续用旧功能。 3. 不依赖后端接口;随静态资源部署即生效。 ### 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 提供): ```json { "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()`。 职责: 1. `import.meta.env.PROD` 为假时直接返回。 2. 请求 `${import.meta.env.BASE_URL}version.json?t=${Date.now()}`,`cache: 'no-store'`。 3. 解析 JSON。`buildId` 为非空字符串且与 `__APP_BUILD_ID__` 不同时弹窗。 4. 弹窗已打开或已经弹过则不再弹。弹出时清除定时器并移除 `visibilitychange` 监听,不再继续检查。 ### 4.2 调度 - 启动后立即检查一次。 - `setInterval` 10 分钟。 - `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. 验证 1. 连续构建两次,两次 `dist/version.json` 的 `buildId` 不同。 2. 用第一版打开页面,再换成第二版静态资源:等待轮询,或切走标签页超过 30 秒后再切回,出现不可关闭的「发现新版本」。 3. 点击「立即刷新」后加载第二版,不再弹窗。 4. 删除或无法访问 `version.json` 时,业务页面仍可操作,控制台不把该失败当成未捕获异常打断应用。 5. `pnpm dev` 不请求 `version.json`。