跳到主要内容

前端应用构建更新检测

“前端应用构建更新检测”本质上不是在问怎么打包,而是在问:新版本已经发布了,浏览器里还跑着旧版本的用户,怎么感知、怎么提示、怎么安全切换?

这是一道非常工程化的题,尤其在 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. 核心心智模型:不可变资源 + 可变探针

最稳的更新检测方案通常都有两个层次:

  1. 不可变资源:JS/CSS/图片文件名带内容 hash,允许长期缓存
  2. 可变探针: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.html
  • asset-manifest.json
  • 运行时配置接口

本质没有变,还是在找一个“可及时更新的探针”。

什么时候不建议直接比对主 bundle URL?

因为主 bundle 通常走强缓存,老页面不一定会主动重新请求它。直接比对它,常常时效性和稳定性都不够好。

3.3 Service Worker 更新事件

如果项目用了 PWA / Service Worker,就不能只做普通轮询,因为还多了一层缓存代理。

这时需要同时关注:

  • registration.updatefound
  • waiting 状态的新 SW
  • controllerchange

常见流程是:

  1. 浏览器发现新 SW
  2. 新 SW 安装完成但进入 waiting
  3. 页面提示用户“发现新版本,是否立即更新”
  4. 用户确认后,让新 SW skipWaiting
  5. 当前页面监听 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.jsonno-cacheno-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

这时通常要做专门兜底:

  1. 捕获 chunk 加载失败
  2. 判断是不是构建切换导致的资源失效
  3. 给出“应用已更新,点击刷新”的提示
  4. 必要时自动刷新一次

如果没有这层兜底,用户感知通常就是“点页面突然白了”。

7. 微前端场景下为什么更难?

因为这时不止一个版本源。

可能同时存在:

  • 主应用版本
  • 子应用版本
  • 共享依赖版本
  • 路由级懒加载资源版本

这时更稳的方式通常是:

  • 每个子应用单独暴露版本信息
  • 主应用统一做版本协调和刷新提示
  • 避免一个子应用偷偷升级导致宿主和子应用协议不匹配

换句话说,更新检测在微前端里最好上升为平台能力,而不是每个子应用各写一套。

8. 面试高频题与标准答法

8.1 前端为什么需要更新检测?

标准答法:

因为前端页面会在浏览器中长期存活,服务端代码更新不会自动让旧页面失效。如果不做更新检测,用户可能一直运行旧版本,导致功能不一致、chunk 404、协议不匹配等问题。

8.2 最稳的更新检测方案是什么?

标准答法:

通常是“静态资源带 hash 做强缓存,入口 HTML 或 version.json 做 no-cache,然后客户端定期检查版本变化”。检测到变化后再根据业务场景选择提示刷新、空闲刷新或故障兜底刷新。

8.3 为什么只给 JS 文件加 hash 还不够?

标准答法:

因为旧页面未必会主动重新请求这些 JS 文件,它需要一个可及时更新的版本探针来感知“现在服务器已经有新版本了”。所以还需要 index.htmlversion.json 或 manifest 这类低缓存入口。

9. 常见坑

  • 只做文件 hash,不做版本探针 结果是资源可缓存,但客户端不知道何时该刷新。
  • version.json 被 CDN 强缓存 结果永远检测不到更新。
  • 检测到更新就立刻强刷 容易打断用户表单、支付、编辑流程。
  • 用了 Service Worker 却没处理 waiting 状态 最终版本切换逻辑失效。
  • 只处理“发现更新”,没处理 chunk 404 用户先看到的是线上故障,而不是升级提示。

10. 速记要点(可背)

  • 更新检测核心 = 版本识别 + 缓存策略 + 切换时机
  • 最稳方案 = hash 资源 + no-cache 探针
  • 常见探针 = index.html / version.json / manifest
  • SW 场景必须处理 updatefound / waiting
  • 检测到新版本后,优先提示刷新而不是直接强刷