欢迎光临
我们一直在努力

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

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

线上发版最尴尬的事:你以为发成功了,结果用户/测试打开的还是旧页面(缓存命中)。
这篇文章给你一套“能自测、能提示更新、能强制刷新、能证明发版成功”的通用方案:
构建时生成版本标识 → 线上暴露 version 文件 → 客户端对比 → 发现更新后提示/强更。


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

常见原因就三类:

  • 静态资源强缓存:index.html 或 js/css 被 CDN/浏览器缓存,用户拿到旧资源
  • Service Worker:PWA/离线缓存没更新,导致一直返回旧资源
  • 灰度/多节点:CDN/多机房更新不一致,用户命中旧节点
  • 所以“判断是否为最新”,本质是:
    让客户端拿到一个“不会被缓存的版本标识”,再和本地版本对比。


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

    2.1 版本标识怎么选?

    推荐两个选择:

    • buildTime:时间戳(如 20251220141600)

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

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

    ✅ 最佳实践:buildId = commitSha + buildTime
    既唯一,又可定位。


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

    适用:本地自测、提测快速确认、排查“我现在到底打开的哪个版本”
    不适用:线上自动提示更新

    3.1 Vite 注入(推荐)

    // 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 注入

    // 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 示例:

    {
    "buildTime": "2025-12-21T14:16:00.000Z",
    "buildId": "a1b2c3d-20251221-1416"
    }

    package.json

    {
    "scripts": {
    "build": "vite build",
    "postbuild": "node scripts/generate-version-dist.mjs"
    }
    }

    scripts/generate-version-dist.mjs

    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 关键实现(精简版)

    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 更新后的处理方式(推荐弹窗 + 刷新)

    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 更新链路

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

    赞(0)
    未经允许不得转载:171主机测评 » 前端部署后如何判断“页面是不是最新”?一套可落地的版本检测方案(适配 Vite/Vue/React/任意 SPA)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址