基于时间戳校验的 SPA 无感知强制刷新方案

0 阅读4分钟

发布了新版本,但用户(尤其在企业微信里)打开的还是旧页面。针对这个问题,我在手动刷新的基础上增加了版本自动更新功能。


1. 构建脚本:生成 version.json 并注入全局变量

1.1 在 public 目录下生成 version.json

我们利用构建工具的钩子,在每次 build 完成后,在 dist 目录(或 public 目录)生成一个 version.json 文件。

方案A:使用 Vite 的 buildEnd 钩子(vite.config.js

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import fs from 'fs'
import path from 'path'

export default defineConfig({
  plugins: [
    vue(),
    {
      name: 'generate-version',
      buildEnd() {
        // 构建结束时,在 dist 目录生成 version.json
        const timestamp = Date.now().toString()
        const distDir = path.resolve(__dirname, 'dist')
        if (!fs.existsSync(distDir)) {
          fs.mkdirSync(distDir, { recursive: true })
        }
        fs.writeFileSync(
          path.join(distDir, 'version.json'),
          JSON.stringify({ buildVersion: timestamp })
        )
        console.log(`✅ version.json generated with buildVersion: ${timestamp}`)
      }
    }
  ],
  define: {
    // 将时间戳注入为全局常量(可在代码中通过 import.meta.env 或直接使用)
    // 注意 Vite 的 define 会把值原样替换,这里转为字符串
    __BUILD_VERSION__: JSON.stringify(Date.now().toString())
  }
})

方案B:使用 Vue CLI 的 chainWebpackvue.config.js

// vue.config.js
const fs = require('fs')
const path = require('path')

module.exports = {
  chainWebpack: config => {
    // 注入全局变量 $version
    config.plugin('define').tap(args => {
      args[0]['$version'] = JSON.stringify(Date.now().toString())
      return args
    })
  },
  configureWebpack: {
    plugins: [
      {
        apply: (compiler) => {
          compiler.hooks.afterEmit.tap('GenerateVersion', (compilation) => {
            const timestamp = Date.now().toString()
            const outputPath = compilation.outputOptions.path
            const versionFile = path.join(outputPath, 'version.json')
            fs.writeFileSync(versionFile, JSON.stringify({ buildVersion: timestamp }))
            console.log(`✅ version.json generated: ${timestamp}`)
          })
        }
      }
    ]
  }
}

注意

  • 使用 public 目录也能实现,但需要将 version.json 放在 public 下,这样构建时会被原样复制到 dist。但为了自动生成时间戳,放在构建钩子中更灵活。
  • 全局变量的注入方式因工具而异,Vite 中我们用 __BUILD_VERSION__,Vue CLI 中可用 $version。下文以前者为例。

2. 前端版本检查逻辑(App.vue 或独立的模块)

2.1 核心代码(Vue 3 组合式 API 写法)

<!-- App.vue -->
<template>
  <router-view />
</template>

<script setup>
import { onMounted, onUnmounted } from 'vue'

// 从全局注入的构建版本(由 vite 的 define 注入)
const BUILD_VERSION = __BUILD_VERSION__  // 若用 Vue CLI 则为 window.$version
const VERSION_CHECK_INTERVAL = 60 * 1000 // 60 秒
const VERSION_URL = '/version.json'      // 服务器上 version.json 的路径
const STORAGE_KEY = 'app_refreshed_version' // 用于记录已刷新过的版本,防死循环

let timer = null

/**
 * 检查版本并决定是否刷新
 */
async function checkVersion() {
  try {
    // 请求时加随机参数,彻底绕过缓存
    const resp = await fetch(`${VERSION_URL}?t=${Date.now()}`)
    if (!resp.ok) throw new Error('Network response was not ok')
    const { buildVersion: serverVersion } = await resp.json()

    // 如果服务器版本与当前 JS 内置版本不一致
    if (serverVersion !== BUILD_VERSION) {
      console.warn(`[版本更新] 服务器: ${serverVersion}, 当前: ${BUILD_VERSION}`)
      
      // 防止死循环:检查是否已经针对这个版本刷新过
      const refreshedVersion = localStorage.getItem(STORAGE_KEY)
      if (refreshedVersion === serverVersion) {
        // 已经刷新过但依然不一致,可能是缓存问题或异常,提示用户手动刷新
        console.error('[版本更新] 已刷新过但版本仍不一致,请手动刷新或清除缓存')
        // 可在此展示一个提示框
        return
      }

      // 记录即将刷新的版本号,然后执行强制刷新
      localStorage.setItem(STORAGE_KEY, serverVersion)
      
      // 方式一:使用 location.reload(true) 强制从服务器加载(部分浏览器已废弃,但多数仍有效)
      // window.location.reload(true)
      
      // 方式二:通过添加时间戳参数强制刷新页面(更可靠)
      const url = new URL(window.location.href)
      url.searchParams.set('_t', Date.now())
      window.location.href = url.toString()
    } else {
      // 版本一致,清除之前记录的刷新标记(可选)
      localStorage.removeItem(STORAGE_KEY)
    }
  } catch (error) {
    // 网络异常或 version.json 不存在时忽略,避免干扰
    console.warn('[版本检查] 请求失败', error)
  }
}

onMounted(() => {
  // 启动后立即检查一次
  checkVersion()
  // 定时轮询
  timer = setInterval(checkVersion, VERSION_CHECK_INTERVAL)
})

onUnmounted(() => {
  if (timer) {
    clearInterval(timer)
    timer = null
  }
})
</script>

2.2 如果需要兼容 Vue 2(选项式 API)

<script>
export default {
  data() {
    return {
      BUILD_VERSION: window.$version || '',
      timer: null,
      VERSION_URL: '/version.json',
      STORAGE_KEY: 'app_refreshed_version',
      CHECK_INTERVAL: 60000
    }
  },
  mounted() {
    this.checkVersion()
    this.timer = setInterval(this.checkVersion, this.CHECK_INTERVAL)
  },
  beforeDestroy() {
    if (this.timer) {
      clearInterval(this.timer)
      this.timer = null
    }
  },
  methods: {
    async checkVersion() {
      try {
        const resp = await fetch(`${this.VERSION_URL}?t=${Date.now()}`)
        if (!resp.ok) throw new Error()
        const { buildVersion } = await resp.json()
        if (buildVersion !== this.BUILD_VERSION) {
          const refreshed = localStorage.getItem(this.STORAGE_KEY)
          if (refreshed === buildVersion) {
            console.warn('已刷新过但仍不一致,请手动操作')
            return
          }
          localStorage.setItem(this.STORAGE_KEY, buildVersion)
          window.location.reload(true) // 或使用加参数方式
        } else {
          localStorage.removeItem(this.STORAGE_KEY)
        }
      } catch (e) {
        // ignore
      }
    }
  }
}
</script>

3. 关键细节说明

3.1 构建版本号的唯一性

  • 使用 Date.now() 时间戳作为版本号,每次构建都会不同,且能天然保证新旧顺序(更大 = 更新)。
  • 如果你希望更语义化的版本号(如 1.2.3),也可以改为从 package.json 读取,但时间戳更简单可靠。

3.2 绕过缓存

  • 请求 version.json 时加上 ?t=时间戳,保证每次都是最新文件。
  • 刷新页面时,我们使用 location.href = location.href + '?_t=' + Date.now() 强制浏览器重新加载所有资源(包括 HTML、JS、CSS),比 reload(true) 更通用。

3.3 防死循环机制

  • 利用 localStorage 记录“已经针对某个版本刷新过”。
  • 当刷新后页面重新加载,BUILD_VERSION 会变成新版本(因为 JS 已更新),此时与服务器版本一致,不会再次刷新。
  • 若因极端缓存问题导致刷新后 JS 仍是旧版本,则会再次进入不一致逻辑,此时 localStorage 中已经记录了该版本号,我们会跳过自动刷新并给出警告,避免无限刷。

3.4 轮询间隔与性能

  • 60 秒检查一次,对服务器压力极小(仅一个 HEADGET 小文件)。
  • 可配置为开发环境更短、生产环境更长。

3.5 使用场景

  • 适用于 SPA(单页应用),尤其部署在 CDN 且带有强缓存的场景。
  • 如果使用了 Service Worker,需要额外处理,但本方案作为基础已足够。

4. 完整流程演示

  1. 构建
    npm run build:prod
    → 自动生成 dist/version.json,内容 {"buildVersion":"1698765432100"}
    → 同时 JS 代码中 __BUILD_VERSION__ 被替换为 "1698765432100"

  2. 部署:将 dist 全部上传到服务器(如 Nginx、OSS)。

  3. 用户打开页面

    • 页面加载,App.vue 启动,立即请求 /version.json
    • 若版本一致,无事发生;若不一致,自动刷新。
  4. 服务端发布新版
    新构建产生新时间戳,部署后,用户最长在 60 秒内自动切换到新版。