核心思路
利用 package.json 中的 version 字段作为版本标识:
-
构建时 :将 version 注入前端代码,同时生成
version.json到产物目录 -
运行时 :轮询服务器上的
version.json,与本地版本对比 -
不一致时:弹窗提示用户刷新,加载新资源
发版:package.json version 递增 → build → dist 含 version.json + 代码内嵌版本号
↓
运行:本地版本(构建写入) vs 远程 version.json(轮询)
↓
更新:版本不一致 → 弹窗 → location.reload()
一、版本号来源
统一从 package.json 读取,发版前手动递增或使用 npm version 自动递增:
bash
# 自动递增并打 git tag(可选)
npm version patch # 1.2.3 → 1.2.4
npm version minor # 1.2.3 → 1.3.0
npm version major # 1.2.3 → 2.0.0
json
// package.json
{
"name": "my-app",
"version": "1.2.3"
}
二、Vue2 接入(vue.config.js)
1. 顶部引入
js
const webpack = require("webpack");
const { version } = require("./package.json");
推荐注入为
process.env.VUE_APP_VERSION,与项目已有的VUE_APP_*环境变量风格一致。
2. configureWebpack:生产环境注入版本号
在现有 configureWebpack 的 production 分支 中,于 config.plugins.push 处加入 DefinePlugin,与 TerserPlugin、CompressionWebpackPlugin 等插件共存:
js
configureWebpack: config => {
if (process.env.NODE_ENV === "production") {
config.mode = "production";
// 注入版本号
config.plugins.push(
new webpack.DefinePlugin({
"process.env.VUE_APP_VERSION": JSON.stringify(version)
})
);
config.optimization = {
// ... 原有 TerserPlugin、splitChunks 等配置
};
config.plugins.push(
new CompressionWebpackPlugin({
// ... 原有压缩配置
})
);
} else {
config.mode = "development";
}
}
3. chainWebpack:构建时生成 version.json
在 chainWebpack 末尾追加,仅生产环境生效,与 DLL、less 全局变量、alias、svg-sprite-loader 等配置互不影响:
js
chainWebpack: config => {
// ... 原有 dll、less、alias、svg 等配置
if (process.env.NODE_ENV === "production") {
config.plugin("version-json").use(
class VersionJsonPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync("VersionJsonPlugin", (compilation, callback) => {
const content = JSON.stringify({
version,
buildTime: new Date().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].css
4. 与现有 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)
js
// vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { readFileSync, writeFileSync } from "fs";
import { resolve } from "path";
const pkg = JSON.parse(readFileSync("./package.json", "utf-8"));
export default defineConfig({
base: "./", // 相对路径部署时与 Vue2 publicPath: './' 等效
plugins: [
vue(),
{
name: "generate-version-json",
closeBundle() {
writeFileSync(
resolve(__dirname, "dist/version.json"),
JSON.stringify({
version: pkg.version,
buildTime: new Date().toISOString()
})
);
}
}
],
define: {
__APP_VERSION__: JSON.stringify(pkg.version)
}
});
也可用
import pkg from "./package.json"(需tsconfig开启resolveJsonModule)。
TypeScript 声明(可选):
ts
// env.d.ts
declare const __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 检测模块
js
// src/utils/versionCheck.js
const LOCAL_VERSION = process.env.VUE_APP_VERSION;
const VERSION_URL = `${process.env.BASE_URL}version.json`;
const CHECK_INTERVAL = 5 * 60 * 1000;
let timer = null;
let hasNotified = false;
async function checkVersion(onUpdate) {
try {
const res = await fetch(`${VERSION_URL}?t=${Date.now()}`, { cache: "no-store" });
if (!res.ok) return;
const { version } = await res.json();
if (version && version !== LOCAL_VERSION && !hasNotified) {
hasNotified = true;
onUpdate(version);
}
} catch (e) {
// 网络异常静默处理,下次轮询再试
}
}
export function startVersionCheck(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);
});
}
export function stopVersionCheck() {
if (timer) clearInterval(timer);
}
3. Vue3 检测模块
js
// src/utils/versionCheck.js
const LOCAL_VERSION = __APP_VERSION__;
const VERSION_URL = `${import.meta.env.BASE_URL}version.json`;
const CHECK_INTERVAL = 5 * 60 * 1000;
let timer = null;
let hasNotified = false;
async function checkVersion(onUpdate) {
try {
const res = await fetch(`${VERSION_URL}?t=${Date.now()}`, { cache: "no-store" });
if (!res.ok) return;
const { version } = await res.json();
if (version && version !== LOCAL_VERSION && !hasNotified) {
hasNotified = true;
onUpdate(version); // onUpdate(version) = 「发现新版本后的处理函数」;versionCheck.js 只负责检测并调用它,具体弹窗、刷新等逻辑在 main.js 传入的回调里写。
}
} catch (e) {
// 静默失败
}
}
export function startVersionCheck(onUpdate) {
if (!import.meta.env.PROD) return;
checkVersion(onUpdate);
timer = setInterval(() => checkVersion(onUpdate), CHECK_INTERVAL);
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") checkVersion(onUpdate);
});
}
export function stopVersionCheck() {
if (timer) clearInterval(timer);
}
4. 入口挂载
Vue2(Element UI 示例):
js
// main.js
import { 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 示例):
js
// main.js
import { 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 可长期缓存:
nginx
# 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 去重弹窗
js
const notifiedKey = "notified_version";
function shouldNotify(newVersion) {
if (localStorage.getItem(notifiedKey) === newVersion) return false;
localStorage.setItem(notifiedKey, newVersion);
return true;
}
// 在 checkVersion 中判断
if (version && version !== LOCAL_VERSION && shouldNotify(version)) {
onUpdate(version);
}
2. 一键发版脚本
json
// 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 禁用强缓存