PWA 如何让用户及时更新到最新版本?
PWA 更新的核心不是“部署完用户就立刻看到新代码”,而是要理解 Service Worker 的生命周期:新脚本会先 install,再进入 waiting,只有旧页面关闭或主动 skipWaiting 后才会 activate。真正稳妥的做法是:HTML 走网络优先,带 hash 的静态资源走缓存优先,发现新 Service Worker 后提示用户刷新,而不是悄悄把正在使用的页面换掉。这样牺牲了一点即时性,但能避免用户填到一半的表单、正在支付的订单被强制刷新打断。
更新链路应该怎么设计
浏览器会在页面导航、注册更新、事件触发以及大约 24 小时后检查 sw.js 是否变化。要注意,只有 Service Worker 文件本身或它 import 的脚本内容变化,才会触发更新流程;单纯改了被缓存的业务 JS,如果缓存策略不对,用户仍可能拿到旧资源。实践里建议把构建产物做文件名 hash,把缓存名跟版本绑定,并在主线程监听 updatefound。更新提示不要只写“刷新”,最好带上版本号和变更摘要,用户才知道是否值得马上中断当前操作。
js// main.js const reg = await navigator.serviceWorker.register('/sw.js'); reg.addEventListener('updatefound', () => { const worker = reg.installing; worker?.addEventListener('statechange', () => { if (worker.state === 'installed' && navigator.serviceWorker.controller) { showReloadTip(() => worker.postMessage({ type: 'SKIP_WAITING' })); } }); }); navigator.serviceWorker.addEventListener('controllerchange', () => location.reload());
js// sw.js const CACHE = 'app-v2026-05-31'; self.addEventListener('message', e => { if (e.data?.type === 'SKIP_WAITING') self.skipWaiting(); }); self.addEventListener('activate', e => { e.waitUntil(caches.keys().then(keys => Promise.all( keys.filter(k => k.startsWith('app-') && k !== CACHE).map(k => caches.delete(k)) )).then(() => self.clients.claim())); });
缓存策略要和发布策略配套
如果前端使用 Vite、Webpack 或 Next.js,静态资源通常会带内容 hash,这类文件可以长期缓存;入口 HTML、manifest、sw.js 则应该保持较短缓存或直接走网络。服务端还要避免 CDN 把 sw.js 缓很久,否则浏览器检查到的永远是旧 Service Worker。一个常见配置是:sw.js 设置 Cache-Control: no-cache,hash 资源设置 max-age=31536000, immutable,HTML 设置 no-cache 或较短时间。这里的取舍很现实:资源缓存越激进,首屏越快;入口缓存越保守,更新越及时。
追问
为什么不直接在 install 里调用 skipWaiting?
可以,但只适合纯展示型页面或内部工具。skipWaiting 会让新 Service Worker 尽快接管旧页面,如果新旧资源协议不兼容,页面可能同时运行旧 JS 和新缓存规则。电商、表单、编辑器这类有未保存状态的页面,更适合弹出“有新版本,刷新后生效”。取舍点在于:即时修复 bug 和不中断用户操作,哪个风险更大。
HTML、JS、图片应该用同一种缓存策略吗?
不应该。HTML 决定入口和资源引用,通常用网络优先,否则用户可能一直进入旧版本;带 hash 的 JS/CSS 可以缓存优先,因为文件名变了就代表内容变了。图片、字体这类大资源可以 stale-while-revalidate,先快显示再后台更新。踩坑最多的是把 index.html 也长期缓存,结果线上发版后用户刷新十次还是旧入口。
如何手动检查更新,避免等浏览器 24 小时?
可以在页面获得焦点、用户打开设置页、或后台定时调用 registration.update()。但不要每几秒轮询,Service Worker 更新检查本身也会产生请求,移动端弱网下体验很差。更常见的边界是:普通版本靠焦点触发检查,严重 bug 修复再配合服务端版本接口提示强刷新。这样既不浪费流量,也能覆盖用户长时间不关页面的情况。
版本回滚应该怎么做?
PWA 的回滚不是让浏览器“撤销 Service Worker”,而是重新部署一个旧逻辑版本并提升 sw.js 的内容版本。缓存清理时不要无脑删光所有缓存,可以保留最近一到两个版本,方便资源短暂回退。边界是安全事故或数据结构错误时,必须优先让 HTML 网络优先拿到修复入口,不能指望旧缓存自己恢复。线上还要记录 controllerchange、安装失败和缓存写入失败,方便判断是不是某个版本卡在 waiting。
如何验证用户真的用上了新版本?
只看发布系统成功是不够的,前端应该上报当前应用版本、Service Worker 版本和缓存版本。可以在页面启动时读取构建时注入的版本号,再和服务端版本接口比对;如果差距太大,再提示用户刷新或清理缓存。注意不要把版本检查做成阻塞启动,否则版本接口挂了会拖垮整个应用。更稳的做法是先让用户进入页面,再异步判断是否需要更新。
收束
PWA 更新体验的关键,是让“快”和“稳”保持平衡。静态资源靠 hash 和长期缓存换速度,入口和 Service Worker 靠谨慎缓存换可控更新;小版本可以温和提示,大故障才需要强制刷新。只要把生命周期、缓存头、用户提示和版本监控串起来,PWA 就能做到接近原生应用的更新体验,同时保留 Web 快速发布的优势。