前端版本检测提示更新

1、方案

  • 构建阶段生成 version.json:在 vue.config.js 中提前计算版本号,既注入到前端(process.env.APP_VERSION),也写入输出目录的 version.json
  • 前端轮询比对:应用启动后每 30 秒请求一次 version.json,禁用缓存并携带时间戳,比较版本号;
  • 交互提示:复用 Ant Design Vue 的 Modal.confirm,展示当前/最新版本与环境;
  • 缓存策略:Nginx 对 HTML/version.json 禁止缓存,对 JS/CSS/图片继续长缓存;
  • CI/CD 配合:所有环境沿用既有脚本,只是构建产物目录多了一份实时的 version.json

2、落实

 2.1 跨平台构建脚本配置(Cross Platform Build Script Configuration)

 为了确保 Windows/Linux/macOS 环境都能正确设置时间戳,我们在 package.json 中使用 cross-env 和独立的时间戳脚本:

// package.json 
"scripts": {
  "build": "cross-env BUILD_TIMESTAMP=$(node scripts/get-timestamp.js) NODE_OPTIONS=--openssl-legacy-provider vue-cli-service build --mode production",
  "build-develop": "cross-env BUILD_TIMESTAMP=$(node scripts/get-timestamp.js) NODE_OPTIONS=--openssl-legacy-provider vue-cli-service build --mode develop",
  "build-testing": "cross-env BUILD_TIMESTAMP=$(node scripts/get-timestamp.js) NODE_OPTIONS=--openssl-legacy-provider vue-cli-service build --mode testing",
  "build-release": "cross-env BUILD_TIMESTAMP=$(node scripts/get-timestamp.js) NODE_OPTIONS=--openssl-legacy-provider vue-cli-service build --mode release"
}
// scripts/get-timestamp.js
// 跨平台获取时间戳
const timestamp = new Date().toISOString().slice(0, 16).replace(/[-T:]/g, "")

  关键点:

    • cross-env 确保环境变量在不同操作系统下正确传递
    • 独立的时间戳脚本避免 shell 命令兼容性问题

 2.2  版本号只生成一次(Build-time Deterministic Versioning)

  为了彻底解决"版本号打架"问题,我们在 vue.config.js 中采用了模块加载时确定时间戳的策略:

// vue.config.js
const fs = require("fs")
const path = require("path")
const packageJson = require("./package.json")

// ⚠️ 重要:在模块加载时确定构建时间戳,避免多进程构建时版本号不一致
// Webpack/Vite 多进程构建时,每个 worker 进程独立执行代码,如果多次调用 new Date()
// 可能在不同进程、不同时间点生成不同的时间戳,导致同一构建产物版本号不一致
// 方案:优先使用 CI/CD 传入的 BUILD_TIMESTAMP,否则在模块加载时确定统一时间戳
const BUILD_TIMESTAMP =
  process.env.BUILD_TIMESTAMP ||
  (() => {
    const now = new Date()
    return now.toISOString().slice(0, 16).replace(/[-T:]/g, "")
  })()

// 格式化时间戳:年月日时分(如202310301500)
function getFormattedTimestamp() {
  // 使用模块加载时确定的统一时间戳
  return BUILD_TIMESTAMP
}

// 获取当前环境名称
function getEnvName() {
  return process.env.VUE_APP_ENV || "develop"
}

// 生成复合版本号:基础版本+环境+时间戳
function getAppVersion() {
  const baseVersion = packageJson.version
  const envName = getEnvName()
  const timestamp = getFormattedTimestamp()
  return `${baseVersion}-${envName}-${timestamp}`
}

// ⚠️ 关键:构建阶段仅生成一次版本号和环境标识,保持前端注入与 version.json 完全一致
const buildEnvName = getEnvName()
const buildVersion = getAppVersion()

module.exports = {
  configureWebpack: {
    plugins: [
      new webpack.DefinePlugin({
        "process.env.APP_VERSION": JSON.stringify(buildVersion),
        "process.env.APP_ENV": JSON.stringify(buildEnvName)
      })
    ]
  },
  chainWebpack(config) {
    config.plugin("generate-version-json").use({
      apply(compiler) {
        compiler.hooks.done.tap("GenerateVersionJsonPlugin", () => {
          fs.writeFileSync(
            path.resolve(__dirname, "edu/version.json"),
            JSON.stringify(
              {
                version: buildVersion,
                env: buildEnvName,
                timestamp: new Date().toISOString(),
                publicPath: "/child/edu",
              },
              null,
              2
            )
          )
        })
      }
    })
  }
}

 核心改进点

  1. 优先使用 CI/CD 传入的时间戳:通过 BUILD_TIMESTAMP 环境变量,让 Jenkins 统一管理版本号;
  2. 模块加载时确定兜底时间戳:如果没有传入环境变量,在模块加载时(而非函数调用时)确定时间戳,避免多进程构建时产生不同版本号;
  3. 全局复用版本号buildVersionbuildEnvName 在模块顶层确定后,被 DefinePlugin 和 version.json 生成逻辑共同使用。

 这样即使构建过程持续 5 ~ 10 分钟,注入的版本号和静态文件里的版本仍保持一致。这其实是把"构建产物视为不可变工件"的原则落地——保证任何使用该工件的入口看到的元数据都是同一个快照。

 2.3 版本检查器(Runtime Polling & Cache Busting)

class VersionChecker {
  currentVersion = process.env.APP_VERSION
  publicPath = "/child/edu"
  checkInterval = 30 * 1000
​
  init() {
    console.log(`📌 当前前端版本:${this.currentVersion}(${process.env.APP_ENV})`)
    this.startChecking()
    document.addEventListener("visibilitychange", () => {
      if (document.visibilityState === "visible" && !this.hasNotified) {
        this.checkForUpdate()
      }
    })
  }
​
  async checkForUpdate() {
    const url = `${this.publicPath}/version.json?t=${Date.now()}`
    const response = await fetch(url, { cache: "no-store" })
    if (!response.ok) return
    const latestInfo = await response.json()
    if (latestInfo.version !== this.currentVersion && !this.hasNotified) {
      this.hasNotified = true
      this.stopChecking()
      this.showUpdateModal(latestInfo.version, latestInfo.env)
    }
  }
}

 这里有两个容易被忽略的细节:

  1. fetch 显式加 cache: "no-store",再叠加时间戳参数,防止 CDN / 浏览器任何一层干预;
  2. visibilitychange 监听,保证窗口重新激活时立即比对,避免用户在后台等了很久才看到弹窗。

 入口 main.ts 在应用 mount 之后调用 versionChecker.init(),即可把整个检测链路串起来。

 
posted @ 2025-12-12 15:33  ~逍遥★星辰~  阅读(16)  评论(0)    收藏  举报