#Vue3篇: Vue 项目发版后如何提示用户刷新?基于 package.json 版本号的前端更新检测方案

核心思路

利用 package.json 中的 version 字段作为版本标识

  1. 构建时 :将 version 注入前端代码,同时生成 version.json 到产物目录

  2. 运行时 :轮询服务器上的 version.json,与本地版本对比

  3. 不一致时:弹窗提示用户刷新,加载新资源

    发版: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:生产环境注入版本号

在现有 configureWebpackproduction 分支 中,于 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.htmlversion.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 禁用强缓存
相关推荐
huashengzsj2 小时前
Shopify个人建站如何搭建独立站:从零开始的完整指南
前端·网络·人工智能
程序猿小泓2 小时前
从 Claude Code 学 Agent Harness:一个前端工程师的 AI Agent 学习笔记
前端·人工智能·学习
柒和远方3 小时前
从审计日志到可下载证据包:事务型 Outbox、租约 fencing 与保留水位
前端·后端·架构
卓怡学长3 小时前
w266基于spring boot + vue 圣地延安美食乐享系统
java·数据库·vue.js·spring boot·spring·intellij-idea
minglie13 小时前
npm ali-oss库依赖冲突
前端·npm
CryptoPP4 小时前
BSE股票K线数据接入实战:从接口调用到前端图表展示
大数据·前端·网络·人工智能·websocket·网络协议
没落英雄5 小时前
5. 从零开始搭建一个 AI Agent —— 人机协作与中断恢复
前端·人工智能·架构
品尚公益团队6 小时前
C#在asp.net网页开发中的带cookie登录实现
前端·c#·asp.net
没有了遇见7 小时前
AI Agent 是什么?—— 一文理解 LLM、Memory、Skills、Tools、MCP、Workflow,Context
android·前端·程序员