前言
你是否遇到过这样的场景:
- 产品经理:「这个 Bug 不是修好了吗?用户怎么还在反馈?」
- 你:「用户可能没刷新页面...」
- 产品经理:「那你想办法让他刷新啊!」
今天就来分享一个优雅的解决方案:基于构建时间戳的前端版本更新检测机制,让用户在不知不觉中收到新版本通知。
实现原理
整体方案分为三步:
- 构建阶段 :通过 Vite 插件将构建时间注入到
index.html的<meta>标签中 - 运行时 :监听页面可见性变化(
visibilitychange),当用户切回页面时触发检测 - 版本比对 :通过
fetch获取最新的index.html,解析构建时间并与当前版本对比
plaintext
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Vite 构建 │───▶│ 注入 buildTime │───▶│ 部署上线 │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ 弹窗通知 │◀───│ 时间戳不一致 │◀───│ 用户切回页面 │
└─────────────────┘ └──────────────────┘ └─────────────────┘
代码实现
1. 获取构建时间
首先封装一个获取构建时间的工具函数,使用 dayjs 处理时区:
typescript
// build/config/time.ts
import dayjs from "dayjs"
import utc from "dayjs/plugin/utc.js"
import timezone from "dayjs/plugin/timezone.js"
export function getBuildTime() {
dayjs.extend(utc)
dayjs.extend(timezone)
const buildTime = dayjs.tz(Date.now(), "Asia/Shanghai").format("YYYY-MM-DD HH:mm:ss")
return buildTime
}
2. Vite 插件注入 Meta 标签
通过 Vite 的 transformIndexHtml 钩子,在构建时将时间戳注入 HTML:
typescript
// build/plugins/html.ts
import type { Plugin } from "vite"
import { getBuildTime } from "../config/time"
export function setupHtmlPlugin() {
const buildTime = getBuildTime()
const plugin: Plugin = {
name: "html-plugin",
apply: "build",
transformIndexHtml(html) {
return html.replace(
"<head>",
`<head>\n <meta name="buildTime" content="${buildTime}">`
)
},
}
return plugin
}
构建后的 HTML 会变成:
html
<head>
<meta name="buildTime" content="2024-01-15 14:30:00">
<!-- 其他内容 -->
</head>
3. 核心:版本更新检测与通知
这是最关键的部分,完整代码如下:
typescript
// src/plugins/app.ts
import { h } from "vue"
import { ElNotification, ElButton } from "element-plus"
import { $t } from "@/locales"
import type { NotificationHandle } from "element-plus"
let isShow = false
let Notification: NotificationHandle | null = null
const { DEV: isDev, PROD: isProd, VITE_BASE_URL, VITE_AUTOMATICALLY_DETECT_UPDATE } = import.meta.env
// 弹出更新通知
function notify() {
if (Notification) Notification.close()
Notification = ElNotification({
title: $t("system.updateContent"),
dangerouslyUseHTMLString: true,
message: h("div", [
h(ElButton, {
onClick: () => Notification?.close(),
}, () => $t("system.updateCancel")),
h(ElButton, {
type: "primary",
onClick: () => {
location.reload()
Notification?.close()
},
}, () => $t("system.updateConfirm")),
]),
onClose: () => { isShow = false },
duration: 6000,
})
}
// 从最新的 index.html 中解析构建时间
async function getHtmlBuildTime(): Promise<string | null> {
const baseUrl = VITE_BASE_URL || "/"
try {
// 添加时间戳避免缓存
const res = await fetch(`${baseUrl}index.html?time=${Date.now()}`)
if (!res.ok) {
console.error("getHtmlBuildTime error:", res.status, res.statusText)
return null
}
const html = await res.text()
const match = html.match(/<meta name="buildTime" content="(.*)">/)
return match?.[1] || null
} catch (error) {
console.error("getHtmlBuildTime error:", error)
return null
}
}
// 检查更新
const checkForUpdates = async () => {
if (isShow) return
const buildTime = await getHtmlBuildTime()
const BUILD_TIME = __APP_INFO__.lastBuildTime
// 构建时间相同,无需更新
if (buildTime === BUILD_TIME) {
return
}
isShow = true
notify()
}
// 初始化版本检测
export function setupAppVersionNotification() {
// 开发环境或 Electron 环境不检测
if (isDev || __IS_ELECTRON__) return
const canAutoUpdateApp = VITE_AUTOMATICALLY_DETECT_UPDATE === "Y" && isProd
if (!canAutoUpdateApp) return
// 监听页面可见性变化
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
checkForUpdates()
}
})
}
4. 在应用入口调用
typescript
// main.ts
import { setupAppVersionNotification } from "@/plugins/app"
const app = createApp(App)
// ...其他初始化
setupAppVersionNotification()
方案亮点
| 特性 | 说明 |
|---|---|
| 🎯 精准触发 | 仅在用户切回页面时检测,不会频繁请求 |
| 🚫 防抖处理 | isShow 标志位防止重复弹窗 |
| 🔄 绕过缓存 | URL 添加时间戳参数确保获取最新 HTML |
| 🌍 国际化支持 | 支持多语言通知文案 |
| ⚙️ 可配置 | 通过环境变量控制是否启用 |
| 🖥️ 多端兼容 | 自动排除开发环境和 Electron |
效果展示
当服务器部署了新版本后,用户切回页面会看到如下通知:
plaintext
┌────────────────────────────────────┐
│ 🔔 发现新版本 │
│ │
│ [ 稍后再说 ] [ 立即刷新 ] │
└────────────────────────────────────┘
总结
这个方案的核心思想是:利用构建时间作为版本标识,通过页面可见性事件触发检测,优雅地提醒用户刷新页面。
相比于 WebSocket 长连接或轮询方案,这种实现:
- ✅ 零依赖,纯前端实现
- ✅ 性能开销极小
- ✅ 用户体验友好
- ✅ 实现成本低
希望这个方案能帮助你解决「用户不刷新页面」的痛点!如果觉得有用,欢迎点赞收藏 👍