这是我在真实项目里为「用户不刷新看不到新页面」做的一次完整治理,从方案推演、和产品拉扯,到最终落地 无感知版本更新提示 的全过程。所有代码以 Vite + Vue3 为例。
1. 🎬 开场:深夜工位的"灵魂三问"
产品(10:02 PM) :
新的页面不是刚上线吗,怎么用户还是看到老版本?
我(10:03 PM) :
让用户刷新一下页面,或者清下缓存就好了。
产品(10:04 PM) :
我要告诉用户"请刷新",这体验太差了。能不能自动刷新?
我(OS) :
......(明明就是 SPA + Nginx 静态资源,这事儿不复杂,但真想做好,也不简单。)
2. 🧭 背景与约束
- 架构:Vue3 + Vite 打包,Nginx 托管静态资源
- 缓存 :
index.html走协商缓存 (Etag/Last-Modified),*.js/*.css走强缓存 (Cache-Control+ 文件名带 hash) - 真实场景 :用户长时间停留 在页面不动;前端上线后,不清空缓存刷新就永远看不到新版本
- 风险 :
- 后端接口变更,老页面还在调用旧协议,线上报错
- 修 bug 后,用户还在用旧代码,无法获得修复
- 提示文案错误,必须尽快全量生效
于是,我们需要一个前端可控、无后端强依赖、能温和推动用户刷新的方案。
3. 🕰 时间线:从"拍脑袋"到"可灰度的工程方案"
Day 1:先跟产品吵一架(然后一起找共识)
-
产品 :我就要"用户完全无感知,像原生 App 一样自动更新"。
-
我 :Web 不是 App。强制刷新会打断用户交互,会丢失表单输入数据。
-
折中方案:
- 若当前无风险操作:弹一个"新版本可用"提示 → 用户确认后刷新
- 若风险更新(协议变更、紧急修复) :在下一次用户点击可控元素时触发刷新(给用户一个可预期的时机)
- 对超长驻留页 :默默在后台定时检测版本 ,可选静默预加载资源
4. ⚔️ 解决方案
方案一:轮询检测 index.html 的 Etag
最简单粗暴的办法:
- 定时向服务器请求一次首页 HTML。
- 如果发现 Etag 变化,提示用户刷新。
在App.vue中添加如下代码
js
<script setup>
import { ref, onMounted } from 'vue';
import { Modal } from 'ant-design-vue';
const oldHtmlEtag = ref();
const timer = ref();
const getHtmlEtag = async () => {
const { protocol, host } = window.location;
const res = await fetch(`${protocol}//${host}`, {
headers: { "Cache-Control": "no-cache" },
});
return res.headers.get("Etag");
};
onMounted(async () => {
oldHtmlEtag.value = await getHtmlEtag();
clearInterval(timer.value);
timer.value = setInterval(async () => {
const newHtmlEtag = await getHtmlEtag();
if (newHtmlEtag !== oldHtmlEtag.value) {
Modal.confirm({
title: "检测到新版本,是否更新?",
okText: "更新",
cancelText: "取消",
onOk: () => window.location.reload(),
});
}
}, 30000); // 每 30 秒检测一次
});
</script>
产品(犹豫) :30 秒一次会不会太频繁?
我:不然用户等半天都没感知到更新。
方案二:版本文件 versionData.json
为了减少请求压力,我们想到生成一个 versionData.json 文件,记录版本号。
自定义 Vite 插件 在 plugins/vitePluginCheckVersion.ts:
js
import path from "path";
import fs from "fs";
export function checkVersion() {
return {
name: "vite-plugin-check-version",
buildStart() {
const now = new Date().getTime();
const version = { version: now };
const versionPath = path.join(__dirname, "../public/versionData.json");
fs.writeFileSync(versionPath, JSON.stringify(version), "utf8");
},
};
}
并在 vite.config.ts 中引入:
js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { checkVersion } from "./plugins/vitePluginCheckVersion";
export default defineConfig({
plugins: [
vue(),
checkVersion(),
]
});
在App.vue中定时请求该文件,发现版本号更新则提示刷新页面
js
const timer = ref(null)
const checkUpdate = async () => {
let res = await fetch('/versionData.json', {
headers: {
'Cache-Control': 'no-cache',
},
}).then((r) => r.json())
if (!localStorage.getItem('demo_version')) {
localStorage.setItem('demo_version', res.version)
} else {
if (res.version !== localStorage.getItem('demo_version')) {
localStorage.setItem('demo_version', res.version)
Modal.confirm({
title: '检测到新版本,是否更新?',
content: '新版本内容:' + res.content,
okText: '更新',
cancelText: '取消',
onOk: () => {
window.location.reload()
},
})
}
}
}
onMounted(()=>{
clearInterval(timer.value)
timer.value = setInterval(async () => {
checkUpdate()
}, 30000)
})
方案三:现成库 plugin-web-update-notification
懒人方案,直接用第三方库:
js
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { webUpdateNotice } from '@plugin-web-update-notification/vite';
export default defineConfig({
plugins: [
vue(),
webUpdateNotice({
logVersion: true, // 控制台打印版本
checkInterval: 30 * 1000 // 自定义检查间隔
}),
]
})
- 优点:方便,省时。
- 缺点:但定制化不足,UI 可能不符合项目风格。
5. 🏁 最终选择:方案二 + 提示弹窗
经过与产品多轮拉扯,决定:
- 采用
versionData.json,性能好,可控性强。 - 提示弹窗由我们自定义,用户可选择立即刷新或稍后。
上线当天,产品亲自测试:
- 打开旧页面,静静等待。
- 弹窗出现------
"检测到新版本,是否更新?" - 点了"更新",页面瞬间重载,加载了最新的内容。
产品(满意地点点头) :说道小王,可以加你个wx吗?
看似一个简单的"刷新页面",背后却藏着缓存策略、用户体验、性能优化的多重博弈。