前端部署后如何判断“页面是不是最新”?一套可落地的版本检测方案(适配 Vite/Vue/React/任意 SPA)

前端部署后如何判断"页面是不是最新"?一套可落地的版本检测方案(适配 Vite/Vue/React/任意 SPA)

线上发版最尴尬的事:你以为发成功了,结果用户/测试打开的还是旧页面(缓存命中)

这篇文章给你一套"能自测、能提示更新、能强制刷新、能证明发版成功"的通用方案:
构建时生成版本标识 → 线上暴露 version 文件 → 客户端对比 → 发现更新后提示/强更。


1. 为什么部署后页面可能不是最新?

常见原因就三类:

  1. 静态资源强缓存index.htmljs/css 被 CDN/浏览器缓存,用户拿到旧资源
  2. Service Worker:PWA/离线缓存没更新,导致一直返回旧资源
  3. 灰度/多节点:CDN/多机房更新不一致,用户命中旧节点

所以"判断是否为最新",本质是:
让客户端拿到一个"不会被缓存的版本标识",再和本地版本对比。


2. 总体思路:版本标识(buildId/buildTime)+ 对比 + 处理

2.1 版本标识怎么选?

推荐两个选择:

  • buildTime :时间戳(如 20251220141600

    • 优点:简单直观,肉眼能看懂
    • 缺点:并不能天然保证"唯一"(同一秒多次 build 需要更细粒度)
  • buildId :Git Commit + 时间(如 a1b2c3d-20251221-1416

    • 优点:唯一、可追溯,排查问题一眼定位版本
    • 缺点:需要 CI 传入 Git 信息

✅ 最佳实践:buildId = commitSha + buildTime

既唯一,又可定位。


3. 方案一(轻量):构建时注入变量,在控制台/页面展示

适用:本地自测、提测快速确认、排查"我现在到底打开的哪个版本"

不适用:线上自动提示更新

3.1 Vite 注入(推荐)

js 复制代码
// vite.config.js
import { defineConfig } from 'vite'

const BUILD_TIME = new Date().toISOString()

export default defineConfig({
  define: {
    __BUILD_TIME__: JSON.stringify(BUILD_TIME),
  },
})
// main.js
console.log('[BUILD_TIME]', __BUILD_TIME__)

3.2 Vue CLI 注入

js 复制代码
// vue.config.js
const webpack = require('webpack')
const BUILD_TIME = new Date().toISOString()

module.exports = {
  configureWebpack: {
    plugins: [
      new webpack.DefinePlugin({
        __BUILD_TIME__: JSON.stringify(BUILD_TIME),
      }),
    ],
  },
}

✅ 建议加一个"页面可视化入口":

例如在 About/设置页显示版本号,或在全局错误页展示 buildId,线上排查巨省时间。


4. 方案二(生产可用):生成 version.json(或 version.txt)+ 轮询对比

这是最通用、最稳定的方案:
构建结束生成 dist/version.json,部署后客户端轮询拉取,发现变化就提示刷新/强更。

4.1 生成 version.json(postbuild)

dist/version.json 示例:

json 复制代码
{
  "buildTime": "2025-12-21T14:16:00.000Z",
  "buildId": "a1b2c3d-20251221-1416"
}
package.json
json 复制代码
{
  "scripts": {
    "build": "vite build",
    "postbuild": "node scripts/generate-version-dist.mjs"
  }
}
scripts/generate-version-dist.mjs
js 复制代码
import { writeFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const root = path.resolve(__dirname, '..')

// 你可以在 CI 里注入 COMMIT_SHA
const COMMIT_SHA = process.env.COMMIT_SHA || 'dev'

async function main() {
  const buildTime = new Date().toISOString()
  const buildId = `${COMMIT_SHA}-${buildTime.replace(/[-:TZ.]/g, '').slice(0, 12)}`

  const outPath = path.join(root, 'dist', 'version.json')
  await writeFile(outPath, JSON.stringify({ buildTime, buildId }, null, 2) + '\n', 'utf-8')
}

main().catch((e) => {
  console.error('[generate-version] failed', e)
  process.exit(1)
})

✅ 注意:version.json 不需要提交 Git!

更推荐在 CI 构建产物里生成,随产物一起发布,避免"忘了 commit 版本号文件"。


5. 客户端实现:轮询拉取 + 对比 + 更新策略

你原文的 versionPoller 思路很好,我这里给一个更偏"生产级"的改造点:

  • 禁止缓存cache: 'no-store' + ?t=timestamp
  • 指数退避:失败时逐步延长间隔,防止网络差导致打爆服务
  • 页面不可见暂停:切后台就停,回前台再继续
  • 更新策略可选
    • 软提示:弹窗"有新版本,点击刷新"
    • 半强制:重要业务更新后强制刷新
    • 强制:直接 location.reload(true)(现代浏览器 true 已不严格区分,但语义保留)

5.1 推荐的版本对比逻辑

  • 本地存:localStorage.__BUILD_ID__
  • 远端取:version.json.buildId
  • 不一致:触发更新

5.2 关键实现(精简版)

js 复制代码
export function createVersionPoller({
  versionUrl = '/version.json',
  storageKey = '__VERSION_CHECK__BUILD_ID__',
  intervalMs = 15000,
  maxDelayMs = 60000,
  onUpdate,
  onResult,
  onError,
} = {}) {
  let timer = null
  let stopped = true
  let delayMs = intervalMs

  const readLocal = () => localStorage.getItem(storageKey) || ''
  const writeLocal = (v) => v && localStorage.setItem(storageKey, String(v))

  const isVisible = () => document.visibilityState !== 'hidden'
  const clear = () => timer && clearTimeout(timer)

  async function fetchRemote() {
    const sep = versionUrl.includes('?') ? '&' : '?'
    const url = `${versionUrl}${sep}t=${Date.now()}`
    const res = await fetch(url, { cache: 'no-store', headers: { 'Cache-Control': 'no-cache' } })
    if (!res.ok) throw new Error(`fetch version failed: ${res.status}`)
    return await res.json()
  }

  async function tick() {
    if (stopped || !isVisible()) return

    const localBuildId = readLocal()

    try {
      const data = await fetchRemote()
      const remoteBuildId = String(data?.buildId || '')
      const remoteBuildTime = String(data?.buildTime || '')

      delayMs = intervalMs
      onResult?.({ localBuildId, remoteBuildId, remoteBuildTime, data })

      if (remoteBuildId && remoteBuildId !== localBuildId) {
        writeLocal(remoteBuildId) // 防重复提示
        onUpdate?.({ localBuildId, remoteBuildId, remoteBuildTime, data })
        return
      }
    } catch (e) {
      delayMs = Math.min(maxDelayMs, Math.max(500, delayMs * 2))
      onError?.(e)
    }

    timer = setTimeout(tick, delayMs)
  }

  function onVisibilityChange() {
    if (stopped) return
    clear()
    if (isVisible()) timer = setTimeout(tick, 0)
  }

  function start() {
    if (!stopped) return
    stopped = false
    delayMs = intervalMs
    document.addEventListener('visibilitychange', onVisibilityChange)
    window.addEventListener('beforeunload', stop)
    if (isVisible()) timer = setTimeout(tick, 0)
  }

  function stop() {
    stopped = true
    clear()
    document.removeEventListener('visibilitychange', onVisibilityChange)
    window.removeEventListener('beforeunload', stop)
  }

  return { start, stop }
}

5.3 更新后的处理方式(推荐弹窗 + 刷新)

js 复制代码
createVersionPoller({
  onUpdate: ({ remoteBuildId }) => {
    // 你可以用组件弹窗
    if (confirm(`检测到新版本:${remoteBuildId}\n是否立即刷新?`)) {
      window.location.reload()
    }
  },
}).start()

6. 关键注意点(决定你方案"好不好用")

6.1 version.json 必须尽量"不缓存"

你已经做了 ?t=,还建议在 CDN/网关层对 version.json 单独配置:

  • Cache-Control: no-store 或极短缓存
  • 或者把它放到"不会被 CDN 强缓存"的路径规则里

6.2 不要轮询太频繁

  • 15s~60s 是常见区间
  • 失败退避是必须的(不然弱网用户会把你服务打爆)

6.3 配合"文件名 hash"才是真正意义的缓存正确姿势

生产里真正的最佳实践是:

  • js/css 走 content-hash(长期缓存)
  • index.html 短缓存/不缓存
  • version.json 不缓存
    这样既能充分利用缓存,又能快速感知新版本。

6.4 如果你用了 Service Worker,一定要处理 SW 更新

否则你会发现:version.json 都更新了,但 SW 还是给你吐旧资源。

SW 场景要额外做:

  • skipWaiting
  • clientsClaim
  • 发现新 SW 后提示用户刷新(或自动刷新)

7. 总结

  • 构建时生成版本标识(buildTime/buildId)
  • 部署产物暴露 version.json
  • 客户端轮询对比 + 发现更新后提示刷新
  • 配合缓存策略(hash 静态资源 + index/version 不缓存)
  • 必要时考虑 Service Worker 更新链路

这套方案的核心价值是:
提测/自测能证明"我现在访问的是最新代码",线上能优雅提示用户刷新,避免缓存导致的"伪发版成功"。

相关推荐
小陶来咯44 分钟前
机器人坐下后拍触摸板站起行动指令无效 — Bug 分析
机器人·bug
LabVIEW开发8 小时前
LabVIEW QMH 队列消息处理架构
架构·labview·labview知识·labview功能·labview程序
代码搬运媛8 小时前
Jest 测试框架详解与实现指南
前端
counterxing9 小时前
我把 Codex 里的 Skills 做成了一个 MCP,还支持分享
前端·agent·ai编程
wangqiaowq9 小时前
windows下nginx的安装
linux·服务器·前端
rising start9 小时前
二、全面理解MySQL架构
mysql·架构
之歆9 小时前
DAY_12JavaScript DOM 完全指南(二):实战与性能篇
开发语言·前端·javascript·ecmascript
发现一只大呆瓜9 小时前
Vite凭什么这么快?3分钟带你彻底搞懂 Vite 热更新的幕后黑手
前端·面试·vite
麦客奥德彪10 小时前
Android Skills
架构·ai编程
Maimai1080810 小时前
React如何用 @microsoft/fetch-event-source 落地 SSE:比原生 EventSource 更灵活的实时推送方案
前端·javascript·react.js·microsoft·前端框架·reactjs·webassembly