核心思路
利用package.json中的version字段作为版本标识:
- 构建时:将 version 注入前端代码,同时生成
version.json到产物目录 - 运行时:轮询服务器上的
version.json,与本地版本对比 - 不一致时:弹窗提示用户刷新,加载新资源
发版:package.json version 递增 → build → dist 含 version.json + 代码内嵌版本号 ↓ 运行:本地版本(构建写入) vs 远程 version.json(轮询) ↓ 更新:版本不一致 → 弹窗 → location.reload()一、版本号来源
统一从package.json读取,发版前手动递增或使用npm version自动递增:
# 自动递增并打 git tag(可选)npmversion patch# 1.2.3 → 1.2.4npmversion minor# 1.2.3 → 1.3.0npmversion major# 1.2.3 → 2.0.0// package.json{"name":"my-app","version":"1.2.3"}二、Vue2 接入(vue.config.js)
1. 顶部引入
constwebpack=require("webpack");const{version}=require("./package.json");推荐注入为
process.env.VUE_APP_VERSION,与项目已有的VUE_APP_*环境变量风格一致。
2. configureWebpack:生产环境注入版本号
在现有configureWebpack的production 分支中,于config.plugins.push处加入DefinePlugin,与 TerserPlugin、CompressionWebpackPlugin 等插件共存:
configureWebpack:config=>{if(process.env.NODE_ENV==="production"){config.mode="production";// 注入版本号config.plugins.push(newwebpack.DefinePlugin({"process.env.VUE_APP_VERSION":JSON.stringify(version)}));config.optimization={// ... 原有 TerserPlugin、splitChunks 等配置};config.plugins.push(newCompressionWebpackPlugin({// ... 原有压缩配置}));}else{config.mode="development";}}3. chainWebpack:构建时生成 version.json
在chainWebpack末尾追加,仅生产环境生效,与 DLL、less 全局变量、alias、svg-sprite-loader 等配置互不影响:
chainWebpack:config=>{// ... 原有 dll、less、alias、svg 等配置if(process.env.NODE_ENV==="production"){config.plugin("version-json").use(classVersionJsonPlugin{apply(compiler){compiler.hooks.emit.tapAsync("VersionJsonPlugin",(compilation,callback)=>{constcontent=JSON.stringify({version,buildTime:newDate().toISOString()// 可选,便于排查});compilation.assets["version.json"]={source:()=>content,size:()=>content.length};callback();});}});}}构建后产物结构:
dist/ ├── index.html ├── version.json ← 运行时轮询此文件 ├── js/ │ ├── app.[hash].js │ └── vendor.[hash].js └── css/ └── app.[hash].css4. 与现有 webpack 配置的兼容性
| 现有配置 | 是否冲突 | 说明 |
|---|---|---|
publicPath: './' | ⚠️ 需注意 | 轮询路径必须用process.env.BASE_URL,不能用/version.json |
CompressionWebpackPlugin | ✅ 无冲突 | 仅压缩 js/html/css,不影响 version.json |
| DLL 插件 | ✅ 无冲突 | 版本检测独立于 DLL 预构建 |
drop_console: true | ✅ 无冲突 | 检测模块避免 console.log 即可 |
splitChunks | ✅ 无冲突 | 版本号在独立 version.json 中 |
三、Vue3 接入(vite.config.js)
// vite.config.jsimport{defineConfig}from"vite";importvuefrom"@vitejs/plugin-vue";import{readFileSync,writeFileSync}from"fs";import{resolve}from"path";constpkg=JSON.parse(readFileSync("./package.json","utf-8"));exportdefaultdefineConfig({base:"./",// 相对路径部署时与 Vue2 publicPath: './' 等效plugins:[vue(),{name:"generate-version-json",closeBundle(){writeFileSync(resolve(__dirname,"dist/version.json"),JSON.stringify({version:pkg.version,buildTime:newDate().toISOString()}));}}],define:{__APP_VERSION__:JSON.stringify(pkg.version)}});也可用
import pkg from "./package.json"(需tsconfig开启resolveJsonModule)。
TypeScript 声明(可选):
// env.d.tsdeclareconst__APP_VERSION__:string;四、运行时版本检测
1. 检测策略
| 项 | Vue2 | Vue3 |
|---|---|---|
| 本地版本 | process.env.VUE_APP_VERSION | __APP_VERSION__ |
| 远程版本 | fetch(\${process.env.BASE_URL}version.json)` | fetch(\${import.meta.env.BASE_URL}version.json)` |
| 防缓存 | URL 加?t=${Date.now()},请求头cache: 'no-store' | 同左 |
| 触发时机 | 页面加载 + 定时轮询(5~30 min)+visibilitychange切回 | 同左 |
| 环境限制 | 仅production启用 | 仅import.meta.env.PROD启用 |
2. Vue2 检测模块
// src/utils/versionCheck.jsconstLOCAL_VERSION=process.env.VUE_APP_VERSION;constVERSION_URL=`${process.env.BASE_URL}version.json`;constCHECK_INTERVAL=5*60*1000;lettimer=null;lethasNotified=false;asyncfunctioncheckVersion(onUpdate){try{constres=awaitfetch(`${VERSION_URL}?t=${Date.now()}`,{cache:"no-store"});if(!res.ok)return;const{version}=awaitres.json();if(version&&version!==LOCAL_VERSION&&!hasNotified){hasNotified=true;onUpdate(version);}}catch(e){// 网络异常静默处理,下次轮询再试}}exportfunctionstartVersionCheck(onUpdate){if(process.env.NODE_ENV!=="production")return;checkVersion(onUpdate);timer=setInterval(()=>checkVersion(onUpdate),CHECK_INTERVAL);document.addEventListener("visibilitychange",()=>{if(document.visibilityState==="visible")checkVersion(onUpdate);});}exportfunctionstopVersionCheck(){if(timer)clearInterval(timer);}3. Vue3 检测模块
// src/utils/versionCheck.jsconstLOCAL_VERSION=__APP_VERSION__;constVERSION_URL=`${import.meta.env.BASE_URL}version.json`;constCHECK_INTERVAL=5*60*1000;lettimer=null;lethasNotified=false;asyncfunctioncheckVersion(onUpdate){try{constres=awaitfetch(`${VERSION_URL}?t=${Date.now()}`,{cache:"no-store"});if(!res.ok)return;const{version}=awaitres.json();if(version&&version!==LOCAL_VERSION&&!hasNotified){hasNotified=true;onUpdate(version);// onUpdate(version) = 「发现新版本后的处理函数」;versionCheck.js 只负责检测并调用它,具体弹窗、刷新等逻辑在 main.js 传入的回调里写。}}catch(e){// 静默失败}}exportfunctionstartVersionCheck(onUpdate){if(!import.meta.env.PROD)return;checkVersion(onUpdate);timer=setInterval(()=>checkVersion(onUpdate),CHECK_INTERVAL);document.addEventListener("visibilitychange",()=>{if(document.visibilityState==="visible")checkVersion(onUpdate);});}exportfunctionstopVersionCheck(){if(timer)clearInterval(timer);}4. 入口挂载
Vue2(Element UI 示例):
// main.jsimport{startVersionCheck}from"@/utils/versionCheck";import{MessageBox}from"element-ui";startVersionCheck(newVersion=>{MessageBox.confirm(`发现新版本${newVersion}(当前${process.env.VUE_APP_VERSION}),是否立即刷新?`,"版本更新",{confirmButtonText:"刷新",cancelButtonText:"稍后",type:"info"}).then(()=>location.reload());});Vue3(Element Plus 示例):
// main.jsimport{startVersionCheck}from"@/utils/versionCheck";import{ElMessageBox}from"element-plus";startVersionCheck(newVersion=>{ElMessageBox.confirm(`发现新版本${newVersion}(当前${__APP_VERSION__}),是否立即刷新?`,"版本更新",{confirmButtonText:"刷新",cancelButtonText:"稍后",type:"info"}).then(()=>location.reload());});五、publicPath / base 路径对照
这是最容易踩坑的地方:
| 部署方式 | Vue2 publicPath | Vue3 base | version.json 请求路径 |
|---|---|---|---|
| 根路径部署 | / | / | /version.json |
| 相对路径部署 | ./ | ./ | ${BASE_URL}version.json |
| 子目录部署 | /app/ | /app/ | /app/version.json |
process.env.BASE_URL(Vue2)和import.meta.env.BASE_URL(Vue3)会自动与publicPath/base保持一致,推荐始终通过 BASE_URL 拼接路径。
六、发版流程
1. package.json 递增 version(或 npm version patch) 2. npm run build 3. 部署 dist 目录(确保 version.json 一并上传) 4. 老用户浏览器运行旧 JS(旧版本号) 5. 轮询到新 version.json → 弹窗提示 → 用户刷新 → 加载新资源七、Nginx 缓存配置
index.html和version.json必须禁用强缓存,否则用户永远拿不到新版本信息;带 contenthash 的 JS/CSS 可长期缓存:
# version.json 不缓存 location ~* version\.json$ { add_header Cache-Control "no-cache, no-store, must-revalidate"; } # index.html 不缓存 location = /index.html { add_header Cache-Control "no-cache, no-store, must-revalidate"; } # JS/CSS 带 hash,长期缓存 location ~* \.(js|css)$ { add_header Cache-Control "public, max-age=31536000, immutable"; }八、可选优化
1. 多 Tab 去重弹窗
constnotifiedKey="notified_version";functionshouldNotify(newVersion){if(localStorage.getItem(notifiedKey)===newVersion)returnfalse;localStorage.setItem(notifiedKey,newVersion);returntrue;}// 在 checkVersion 中判断if(version&&version!==LOCAL_VERSION&&shouldNotify(version)){onUpdate(version);}2. 一键发版脚本
// package.json scripts{"release:patch":"npm version patch && npm run build","release:minor":"npm version minor && npm run build"}3. 轮询间隔建议
| 场景 | 建议间隔 |
|---|---|
| 内部管理系统 | 5 ~ 10 分钟 |
| 面向 C 端用户 | 15 ~ 30 分钟 |
| 切回页面时 | 立即检测(visibilitychange) |
九、方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| version.json 轮询(推荐) | 实现简单、框架无关、可控 | 需额外请求;依赖部署缓存策略 |
| 轮询 index.html 解析 hash | 无需额外文件 | 解析复杂、请求体积大 |
| Service Worker | 可静默更新 | 实现成本高,需 PWA 支持 |
十、常见问题
| 问题 | 原因 | 解决 |
|---|---|---|
| 发版后用户无感知 | version.json 被 CDN/Nginx 缓存 | 配置 no-cache |
| fetch 404 | publicPath 为./却用绝对路径/version.json | 改用BASE_URL拼接 |
| 开发环境也弹窗 | 未判断环境 | 加NODE_ENV === 'production'判断 |
| 多标签重复弹窗 | 各 Tab 独立检测 | localStorage 记录已提示版本 |
| 刷新后仍是旧版 | index.html 被缓存 | index.html 配置 no-cache |
十一、最小实现清单
✅ package.json 维护 version 字段 ✅ 构建配置注入版本号(Vue2: VUE_APP_VERSION / Vue3: __APP_VERSION__) ✅ 构建产物生成 version.json ✅ 运行时通过 BASE_URL 轮询对比 + 更新提示 ✅ 仅生产环境启用检测 ✅ Nginx/CDN 对 version.json、index.html 禁用强缓存