从“版本号打架”到 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 我们的诉求
- 用户在 30 秒内感知版本更新;
- 弹窗里能看到“当前版本 / 最新版本 / 环境”;
- 支持“立即刷新 / 稍后再说”,不给用户造成中断;
- 方案需兼容现有微前端架构与 CI/CD 流程,不依赖后端改造。
2. 方案探索与取舍
在动手前,我们列出几种可行方式:
| 方案 | 实现复杂度 | 实时性 | 依赖 | 适配场景 | 关键优缺点 |
|---|---|---|---|---|---|
| 纯前端轮询 version.json | 低 | 中(30s) | 前端 + Nginx | 多环境微前端 | 成本最低;轻微网络开销 |
| Service Worker/PWA | 中 | 较高 | 现代浏览器 | PWA 应用 | 缓存控制好,但改造量大 |
| WebSocket 推送 | 高 | 最高 | 后端服务 | 强实时场景 | 需要额外服务端开发 |
| 后端接口统一管理 | 中 | 中 | 前后端 | 版本集中管理 | 带来跨团队耦合 |
综合团队资源与落地速度,我们选择了 纯前端轮询 + 静态版本文件 的做法,并明确两个原则:
-
版本号唯一,可追溯:
基础版本号-环境-时间戳; -
发布零侵入:Jenkins 仍旧运行
npm run build-xxx,无需新增步骤。
3. 技术方案总览
-
构建阶段生成 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。
4. 关键落地细节
4.1 版本号只生成一次(Build-time Deterministic Versioning)
vue.config.js 抽象 buildEnvName、buildVersion,并在 DefinePlugin 与生成 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
)
);
});
},
});
},
};
这样即使构建过程持续 5~10 分钟,注入的版本号和静态文件里的版本仍保持一致。这其实是把“构建产物视为不可变工件”的原则落地——保证任何使用该工件的入口看到的元数据都是同一个快照。
4.2 版本检查器(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);
}
}
}
这里有两个容易被忽略的细节:
-
fetch显式加cache: "no-store",再叠加时间戳参数,防止 CDN / 浏览器任何一层干预; -
visibilitychange监听,保证窗口重新激活时立即比对,避免用户在后台等了很久才看到弹窗。
入口 main.ts 在应用 mount 之后调用 versionChecker.init(),即可把整个检测链路串起来。
4.3 Nginx 缓存策略(Precise Cache Partition)
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. 测试与验证
我们定义了一个完整的回归流程,确保方案不会给测试和上线带来额外负担:
-
首次访问:打开 dev 环境页面,确认控制台打印版本号,Network 里能看到
version.json且响应头无缓存; -
触发新版本:调整任意文案,重新发布,保持旧页面不刷新;
-
轮询验证:30 秒内弹出提示框,展示当前/最新版本和环境;
-
交互路径:
- 点击“立即刷新”:页面强制 reload,新版本生效;
- 点击“稍后刷新”:记录取消动作并重新开启轮询;
-
边界场景:切 tab / 清缓存 / 新设备访问 / 短时间连续发布,均能正确感知最新版本。
6. 注意事项与常见问题
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 没有弹窗 |
version.json 404 或版本未变 |
检查部署路径、确认构建是否生成文件 |
| 弹窗后刷新仍旧版本 | 静态资源被缓存 | 核实 Nginx 缓存策略、查看浏览器缓存设置 |
| 构建失败 |
cross-env 未安装或权限不足 |
补充依赖、确保 Jenkins 工作目录可写 |
| 持续误报更新 | 构建阶段多次生成版本号 | 在 vue.config.js 顶部缓存 buildVersion 并全局复用 |
7. 落地成效
- 旧页面用户在 30 秒内收到提醒,测试效率显著提升;
- “幽灵弹窗”彻底消失,版本对比逻辑稳定;
- 方案只触碰前端与 Nginx 配置,发布流程无需改造;
- 文档化后,其他子应用无需重复思考,直接复用。
8. 展望
下一步我们计划:
- 封装通用 SDK:抽象版本生成、轮询、弹窗逻辑,支持 Vue CLI / Vite;
- 可视化版本面板:在主应用汇总所有环境的版本和发布时间;
- 差异化策略:针对高优先级版本强制刷新,普通版本允许用户自行选择。
这次实践让我再次意识到:真正的坑往往藏在看似“微不足道”的细节里。当我们把问题和思考写成文档、沉淀成模板,团队就能以更小的代价获得更稳定的交付。如果你也在推进微前端版本同步,欢迎交流、互相借鉴。