前端骚操作:用户还在摸鱼,新版本已悄悄上线!一招实现无感知版本更新通知

前言

你是否遇到过这样的场景:

  • 产品经理:「这个 Bug 不是修好了吗?用户怎么还在反馈?」
  • 你:「用户可能没刷新页面...」
  • 产品经理:「那你想办法让他刷新啊!」

今天就来分享一个优雅的解决方案:基于构建时间戳的前端版本更新检测机制,让用户在不知不觉中收到新版本通知。

实现原理

整体方案分为三步:

  1. 构建阶段 :通过 Vite 插件将构建时间注入到 index.html<meta> 标签中
  2. 运行时 :监听页面可见性变化(visibilitychange),当用户切回页面时触发检测
  3. 版本比对 :通过 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 长连接或轮询方案,这种实现:

  • ✅ 零依赖,纯前端实现
  • ✅ 性能开销极小
  • ✅ 用户体验友好
  • ✅ 实现成本低

希望这个方案能帮助你解决「用户不刷新页面」的痛点!如果觉得有用,欢迎点赞收藏 👍

相关推荐
想个什么名好呢20 分钟前
解决uniapp的H5项目uni-popup页面滚动穿透bug
前端
用户938169125536032 分钟前
Vue3项目--mock数据
前端
前端加油站1 小时前
一种新HTML 页面转换成 PDF 技术方案
前端·javascript·vue.js
w***Q3501 小时前
Vue打包
前端·javascript·vue.js
有事没事实验室1 小时前
router-link的custom模式
前端·javascript·vue.js
常乐我净6161 小时前
十、路由和导航
前端
Tonychen1 小时前
TypeScript 里 infer 常见用法
前端·typescript
米诺zuo1 小时前
MUI sx prop 中的响应式适配
前端
周尛先森1 小时前
都React 19了,他到底带来了什么?
前端