前端应用构建更新检测
“前端应用构建更新检测”本质上不是在问怎么打包,而是在问:新版本已经发布了,浏览器里还跑着旧版本的用户,怎么感知、怎么提示、怎么安全切换?
这是一道非常工程化的题,尤其在 SPA、微前端、Service Worker、灰度发布场景里特别常见。
0. 面试速答(30 秒版 TL;DR)
- 更新检测的核心不是“轮询一下接口”这么简单,而是要同时解决 版本识别、缓存策略、切换时机、用户体验 4 个问题。
- 最稳的思路是:静态资源文件名带 hash 做强缓存,入口 HTML /
version.json/ 清单文件走 no-cache,然后客户端定期检查版本号是否变化。 - 检测到新版本后,不要直接强刷页面,通常要根据场景做:
- 轻提示“发现新版本,点击刷新”
- 空闲时刷新
- 出现 chunk 加载失败时兜底刷新
- 如果用了 Service Worker,还要把 SW 更新事件 一起纳入方案,否则你会遇到“版本明明发了,但页面还拿旧缓存”的问题。
1. 先把问题讲清:为什么前端会出现“旧版本存活”?
前端和后端最大的不同之一是:
- 后端一发布,下一次请求通常天然命中新代码
- 前端发布后,用户浏览器里已经加载好的页面不会自动消失
所以发布后常见状态是:
- 服务器上已经是新版本
- 用户 tab 里还跑着旧版本
- 路由懒加载的 chunk、接口协议、静态资源清单可能已经不一致
这时就会出现几类故障:
- 页面还在运行,但功能逻辑已落后
- 点击新路由时老页面去加载旧 chunk,结果 404
- 前后端协议不兼容,页面行为异常
- Service Worker 仍然回旧缓存,导致“怎么刷新都不对”
2. 核心心智模型:不可变资源 + 可变探针
最稳的更新检测方案通常都有两个层次:
- 不可变资源:JS/CSS/图片文件名带内容 hash,允许长期缓存
- 可变探针:HTML、
version.json、manifest 等用于告诉客户端“当前版本号是多少”的轻量入口,必须可及时更新
先记这一条链路:资源负责稳定缓存,探针负责发现更新。
面试里建议直接把这句说出来:
- 资源文件负责稳定缓存,版本探针负责及时发现更新。
3. 最常见的几种更新检测方案
3.1 轮询 version.json
这是最常见、也最容易落地的方案。
构建时生成一个轻量版本文件,例如:
{
"version": "2026-03-23T10:30:00Z",
"buildId": "git-sha-abcdef"
}
客户端启动后:
- 记录当前内置版本号
- 每隔一段时间请求一次
version.json - 如果远端版本变了,就提示用户刷新
优点:
- 简单、稳定、和框架无关
- 可用于 Vite、Webpack、Next.js、微前端壳应用等多种场景
缺点:
- 有轮询开销
- 只能“最终感知”,不是实时推送
3.2 比对入口 HTML 或资源清单
如果你不想单独维护 version.json,也可以把版本信息放在:
index.htmlasset-manifest.json- 运行时配置接口
本质没有变,还是在找一个“可及时更新的探针”。
什么时候不建议直接比对主 bundle URL?
因为主 bundle 通常走强缓存,老页面不一定会主动重新请求它。直接比对它,常常时效性和稳定性都不够好。
3.3 Service Worker 更新事件
如果项目用了 PWA / Service Worker,就不能只做普通轮询,因为还多了一层缓存代理。
这时需要同时关注:
registration.updatefoundwaiting状态的新 SWcontrollerchange
常见流程是:
- 浏览器发现新 SW
- 新 SW 安装完成但进入
waiting - 页面提示用户“发现新版本,是否立即更新”
- 用户确认后,让新 SW
skipWaiting - 当前页面监听
controllerchange后刷新
否则会出现一种经典现象:
- 代码已经上线
- 你也检测到了新版本
- 但页面依然被旧 SW 控着,结果刷新后还是旧资源
3.4 WebSocket / SSE 实时推送
内部后台、实时控制台、运营平台有时会用这种方案。
优点:
- 几乎实时
- 不必高频轮询
缺点:
- 需要服务端长连接支持
- 运维和连接稳定性成本更高
这类方案更适合“内网系统 / 高频在线系统”,普通 ToC 前端一般不必默认上。
4. 一套更稳的落地方案
如果面试官问“你会怎么做”,推荐按下面这套答:
第一步:构建时注入版本号
版本号可以来自:
- Git commit SHA
- CI build number
- 发布时间戳
目标是让前端运行时知道“自己当前是什么版本”。
第二步:生成版本探针文件
例如 version.json:
{
"version": "abcdef",
"builtAt": "2026-03-23T10:30:00Z"
}
第三步:给缓存策略分层
main.[hash].js/chunk.[hash].js:长缓存index.html/version.json:no-cache或no-store
第四步:客户端轮询或在页面恢复焦点时检查
常见触发时机:
- 页面首次加载后定时轮询
- 浏览器 tab 从后台切回前台时检查
- 网络重连时检查
第五步:检测到新版本后不要立刻硬刷
更稳的策略通常是:
- 有表单编辑、支付、长流程操作时先提示,不强刷
- 在空闲页、列表页、只读页可以更积极
- 某些故障场景再自动兜底刷新
5. 一个最小可用实现
下面这个示例是框架无关的思路:
const CURRENT_VERSION = __APP_VERSION__
let hasPrompted = false
async function fetchRemoteVersion() {
const res = await fetch(`/version.json?t=${Date.now()}`, {
cache: 'no-store',
})
if (!res.ok) {
return null
}
const data = await res.json()
return data.version ?? null
}
async function checkForUpdate() {
const remoteVersion = await fetchRemoteVersion()
if (!remoteVersion || remoteVersion === CURRENT_VERSION || hasPrompted) {
return
}
hasPrompted = true
const shouldReload = window.confirm('发现新版本,是否立即刷新页面?')
if (shouldReload) {
window.location.reload()
}
}
setInterval(checkForUpdate, 5 * 60 * 1000)
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
void checkForUpdate()
}
})
这个实现的关键点不是代码本身,而是:
- 带时间戳避免缓存
- 版本号来自构建注入
- 不检测到就刷新
- 切回前台时也做检查
6. 更新检测还要处理“故障型刷新”
有些用户不会等到你轮询到新版本,先遇到的是异常。
最经典的就是:
- 路由懒加载去请求旧 chunk
- 线上已发布新版本,旧 chunk 文件被清理
- 浏览器报
Loading chunk failed
这时通常要做专门兜底:
- 捕获 chunk 加载失败
- 判断是不是构建切换导致的资源失效
- 给出“应用已更新,点击刷新”的提示
- 必要时自动刷新一次
如果没有这层兜底,用户感知通常就是“点页面突然白了”。
7. 微前端场景下为什么更难?
因为这时不止一个版本源。
可能同时存在:
- 主应用版本
- 子应用版本
- 共享依赖版本
- 路由级懒加载资源版本
这时更稳的方式通常是:
- 每个子应用单独暴露版本信息
- 主应用统一做版本协调和刷新提示
- 避免一个子应用偷偷升级导致宿主和子应用协议不匹配
换句话说,更新检测在微前端里最好上升为平台能力,而不是每个子应用各写一套。
8. 面试高频题与标准答法
8.1 前端为什么需要更新检测?
标准答法:
因为前端页面会在浏览器中长期存活,服务端代码更新不会自动让旧页面失效。如果不做更新检测,用户可能一直运行旧版本,导致功能不一致、chunk 404、协议不匹配等问题。
8.2 最稳的更新检测方案是什么?
标准答法:
通常是“静态资源带 hash 做强缓存,入口 HTML 或 version.json 做 no-cache,然后客户端定期检查版本变化”。检测到变化后再根据业务场景选择提示刷新、空闲刷新或故障兜底刷新。
8.3 为什么只给 JS 文件加 hash 还不够?
标准答法:
因为旧页面未必会主动重新请求这些 JS 文件,它需要一个可及时更新的版本探针来感知“现在服务器已经有新版本了”。所以还需要 index.html、version.json 或 manifest 这类低缓存入口。
9. 常见坑
- 只做文件 hash,不做版本探针 结果是资源可缓存,但客户端不知道何时该刷新。
version.json被 CDN 强缓存 结果永远检测不到更新。- 检测到更新就立刻强刷 容易打断用户表单、支付、编辑流程。
- 用了 Service Worker 却没处理 waiting 状态 最终版本切换逻辑失效。
- 只处理“发现更新”,没处理 chunk 404 用户先看到的是线上故障,而不是升级提示。
10. 速记要点(可背)
- 更新检测核心 = 版本识别 + 缓存策略 + 切换时机
- 最稳方案 = hash 资源 + no-cache 探针
- 常见探针 =
index.html/version.json/ manifest - SW 场景必须处理
updatefound/waiting - 检测到新版本后,优先提示刷新而不是直接强刷