发布了新版本,但用户(尤其在企业微信里)打开的还是旧页面。针对这个问题,我在手动刷新的基础上增加了版本自动更新功能。
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 的 chainWebpack(vue.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 秒检查一次,对服务器压力极小(仅一个
HEAD或GET小文件)。 - 可配置为开发环境更短、生产环境更长。
3.5 使用场景
- 适用于 SPA(单页应用),尤其部署在 CDN 且带有强缓存的场景。
- 如果使用了 Service Worker,需要额外处理,但本方案作为基础已足够。
4. 完整流程演示
-
构建:
npm run build:prod
→ 自动生成dist/version.json,内容{"buildVersion":"1698765432100"}
→ 同时 JS 代码中__BUILD_VERSION__被替换为"1698765432100" -
部署:将
dist全部上传到服务器(如 Nginx、OSS)。 -
用户打开页面:
- 页面加载,
App.vue启动,立即请求/version.json。 - 若版本一致,无事发生;若不一致,自动刷新。
- 页面加载,
-
服务端发布新版:
新构建产生新时间戳,部署后,用户最长在 60 秒内自动切换到新版。