从“版本号打架”到 30 秒内提醒用户刷新:一个微前端团队的实践

从"版本号打架"到 30 秒内提醒用户刷新:一个微前端团队的实践

1. 背景与痛点

我们团队维护着一个微前端子应用集群,每个子应用都需要同时服务 dev / test / release / online 多套环境。分支策略(master / release / test / dev / hotfix / feature_x.x.x)加上 Jenkins 自动化,让"一天多次发布"成为常态。但真正影响交付效率的并不是发布次数,而是一个顽固的问题:测试同学常年停留在旧版本页面

1.1 真实场景

  • 测试在早上打开 dev 页面,下午我们发布了新的组件样式;
  • 他们继续在旧页面里回归,反馈的问题我们一眼看出"这是老版本";
  • 群里喊"刷新一下"并不靠谱,于是"无效缺陷 + 反复沟通"成了常态。

更严重的一次事故,是我们在版本检查逻辑里同时使用了 webpack DefinePlugin 与自定义插件,各自调用了一次 getAppVersion()。结果前端控制台打印的是 0.8.3-release-202511210828,而 version.json 里是 0.8.3-release-202511210829。两边只差 1 秒钟,却让线上用户始终被提示刷新,形象地被团队称为"版本号打架"。

1.2 我们的诉求

  1. 用户在 30 秒内感知版本更新;
  2. 弹窗里能看到"当前版本 / 最新版本 / 环境";
  3. 支持"立即刷新 / 稍后再说",不给用户造成中断;
  4. 方案需兼容现有微前端架构与 CI/CD 流程,不依赖后端改造。

2. 方案探索与取舍

在动手前,我们列出几种可行方式:

方案 实现复杂度 实时性 依赖 适配场景 关键优缺点
纯前端轮询 version.json 中(30s) 前端 + Nginx 多环境微前端 成本最低;轻微网络开销
Service Worker/PWA 较高 现代浏览器 PWA 应用 缓存控制好,但改造量大
WebSocket 推送 最高 后端服务 强实时场景 需要额外服务端开发
后端接口统一管理 前后端 版本集中管理 带来跨团队耦合

综合团队资源与落地速度,我们选择了 纯前端轮询 + 静态版本文件 的做法,并明确两个原则:

  • 版本号唯一,可追溯基础版本号-环境-时间戳
  • 发布零侵入 :Jenkins 仍旧运行 npm run build-xxx,无需新增步骤。

3. 技术方案总览

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

4. 关键落地细节

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

vue.config.js 抽象 buildEnvNamebuildVersion,并在 DefinePlugin 与生成 version.json 时复用:

javascript 复制代码
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
            )
          );
        });
      },
    });
  },
};

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

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

javascript 复制代码
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(),即可把整个检测链路串起来。

4.3 Nginx 缓存策略(Precise Cache Partition)

nginx 复制代码
location / {
    if ($request_filename ~* .html$) {
        add_header Cache-Control "no-store, no-cache, must-revalidate";
    }
}
​
location /child/edu {
    if ($request_filename ~* .html$) {
        add_header Cache-Control "no-store, no-cache, must-revalidate";
    }
}
​
location ~* /child/edu/version.json$ {
    add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate";
    add_header Pragma "no-cache";
    add_header Expires "0";
    add_header Surrogate-Control "no-store";
}

这一层的思路是把资源分成两类:需要实时性(HTML、version.json)就 no-store,其余走长缓存。再配合 try_files 兜底 history 路由,微前端子应用的独立部署不会互相影响。

4.4 CI/CD 配置(Zero-touch Pipeline)

环境 构建命令 输出路径 说明
develop npm run build-develop /child/edu 日常开发验证
testing npm run build-testing /child/edu 集成测试
release npm run build-release /child/edu 预发布
production npm run build-production /child/edu 线上

所有命令都带 cross-env NODE_OPTIONS=--openssl-legacy-provider,以兼容不同系统的 OpenSSL 版本。更重要的是,这套方案没有"要求运维多做一步"------构建产物天然携带 version.json,任何环境拿到包即可上线。

5. 测试与验证

我们定义了一个完整的回归流程,确保方案不会给测试和上线带来额外负担:

  1. 首次访问 :打开 dev 环境页面,确认控制台打印版本号,Network 里能看到 version.json 且响应头无缓存;

  2. 触发新版本:调整任意文案,重新发布,保持旧页面不刷新;

  3. 轮询验证:30 秒内弹出提示框,展示当前/最新版本和环境;

  4. 交互路径

    • 点击"立即刷新":页面强制 reload,新版本生效;
    • 点击"稍后刷新":记录取消动作并重新开启轮询;
  5. 边界场景:切 tab / 清缓存 / 新设备访问 / 短时间连续发布,均能正确感知最新版本。

6. 注意事项与常见问题

现象 可能原因 解决方案
没有弹窗 version.json 404 或版本未变 检查部署路径、确认构建是否生成文件
弹窗后刷新仍旧版本 静态资源被缓存 核实 Nginx 缓存策略、查看浏览器缓存设置
构建失败 cross-env 未安装或权限不足 补充依赖、确保 Jenkins 工作目录可写
持续误报更新 构建阶段多次生成版本号 vue.config.js 顶部缓存 buildVersion 并全局复用

7. 落地成效

  • 旧页面用户在 30 秒内收到提醒,测试效率显著提升;
  • "幽灵弹窗"彻底消失,版本对比逻辑稳定;
  • 方案只触碰前端与 Nginx 配置,发布流程无需改造;
  • 文档化后,其他子应用无需重复思考,直接复用。

8. 展望

下一步我们计划:

  1. 封装通用 SDK:抽象版本生成、轮询、弹窗逻辑,支持 Vue CLI / Vite;
  2. 可视化版本面板:在主应用汇总所有环境的版本和发布时间;
  3. 差异化策略:针对高优先级版本强制刷新,普通版本允许用户自行选择。

这次实践让我再次意识到:真正的坑往往藏在看似"微不足道"的细节里。当我们把问题和思考写成文档、沉淀成模板,团队就能以更小的代价获得更稳定的交付。如果你也在推进微前端版本同步,欢迎交流、互相借鉴。

相关推荐
恋猫de小郭9 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅15 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606116 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了16 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅16 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅16 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅17 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment17 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅17 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端