题干与适用场景
PWA 发布后,部分用户仍由旧 Service Worker 控制,加载旧 JavaScript 与新 HTML 的组合,出现 ChunkLoadError。请说明生命周期、缓存版本、打开标签页、skipWaiting、clientsClaim 和回滚之间的关系。
这道题适合前端、Web 平台和全栈岗位。题目要求讨论用户体验与部署安全,不要求绑定某个框架。错误率和旧缓存比例是练习数据,不是固定阈值。
面试官考察点
生命周期
候选人要说清 install、waiting、activate 和控制页面的先后关系,尤其是更新 worker 默认等待旧客户端退出。
资产一致性
要区分 HTML、hashed JavaScript、运行时 API 和 precache,避免清理旧缓存后仍有页面引用旧资源。
采用策略
skipWaiting 可以加快接管,但可能让同一页面前后使用不同版本;强回答会先评估兼容性和用户提示。
回滚与观测
需要有版本标识、错误监控、缓存保留窗口和 no-op worker 方案,确保坏版本能停止扩散。
回答前需要澄清的问题
- HTML 和静态资源是否带内容 hash?
- Service Worker 脚本 URL 和 scope 是否稳定?
- 页面是否可能长时间打开或有未保存状态?
- API 是否向后兼容两个前端版本?
- 是否允许提示用户刷新或安排下一次导航?
- 发布平台能否快速回滚 worker 脚本和资产清单?
30 秒回答框架
“新 worker 安装成功后默认进入 waiting,旧 worker 仍控制已打开页面。我要先让 HTML、hashed assets 和 API 具备版本兼容,再决定是否在用户确认后 skipWaiting。缓存使用版本化命名或 Workbox revision,activate 只删除已知过期缓存。
客户端监听 updatefound 和 controllerchange,提示用户在安全时机刷新,并记录 worker 版本、ChunkLoadError 和激活耗时。发现坏版本时先停止发布或部署 no-op worker,保留旧资源,修复后再推进。”
分步骤深入解答
第一步:画出生命周期
注册后 worker 进入 installing,install 成功后变成 waiting;旧 worker 仍控制现有页面。旧客户端退出或导航后,新 worker 才会 activate 并接管。
第二步:保证资源兼容
HTML 应引用带 hash 的静态资源,API 在迁移窗口内兼容旧字段。不要只改缓存名,却让旧页面请求已经删除的 chunk。
第三步:设计更新检查
保持 worker URL 稳定,浏览器会按导航和注册检查脚本字节变化。长会话可以显式调用 registration.update,但不要频繁轮询。
~~~js const registration = await navigator.serviceWorker.ready; await registration.update(); ~~~
第四步:处理等待与提示
默认等待最安全;若产品必须立即更新,先检测未保存状态和页面兼容性,再通过 postMessage 让用户确认 skipWaiting。controllerchange 后整页刷新应避免循环。
第五步:清理与回滚
activate 中只删除 allowlist 之外的缓存。保留上一版静态资源和 manifest,发生错误时回滚入口或部署 no-op worker,不要直接清空所有缓存。
第六步:观测和验收
记录 worker 版本、缓存命中、激活时间、controllerchange、资源 404 和 ChunkLoadError。灰度验证新旧页面组合、离线启动、多个标签页和回滚路径。
高质量示范回答
“ChunkLoadError 很可能来自旧 worker 控制的页面与新部署资产不兼容。新 worker 安装后会 waiting,默认要等旧客户端退出或导航,所以我不会先全局 skipWaiting。
我会使用稳定的 worker URL、带 hash 的静态资源和一段 API 兼容窗口。预缓存按 revision 管理,activate 只清理明确过期的缓存。客户端发现 waiting worker 时提示用户在没有未保存状态的时机刷新;若必须立即切换,先确认新旧页面协议兼容,再发送 skipWaiting,并处理 controllerchange。
发布按版本灰度,监控 worker 版本、资源 404 和 ChunkLoadError。若新版本异常,停止入口发布或部署 no-op worker,同时保留旧资源,修复后再恢复推进。验收包括新旧标签页、离线启动、刷新和回滚。”
常见错误
- 认为 install 成功就会立即控制所有页面。
- 不加 hash,直接覆盖旧 JavaScript。
- 无条件 skipWaiting,导致同一页面混用新旧版本。
- activate 直接清空所有缓存。
- 改名 worker 脚本来“强制更新”,破坏稳定注册。
- 只测试新打开页面,不测试长时间打开的旧标签页。
- API 不兼容却只回滚前端入口。
- 没有版本监控和 no-op 回滚路径。
追问及应对
追问一:为什么新 worker 会停在 waiting?
因为旧 worker 仍控制打开的客户端。默认生命周期会等待这些客户端关闭或导航,避免同一页面中途切换控制者。
追问二:skipWaiting 什么时候合理?
只有在资源和页面协议向后兼容、没有未保存状态且产品能接受立即刷新时。否则应等待用户确认或下一次导航。
追问三:如何避免旧缓存无限增长?
使用版本或 revision allowlist,在 activate 删除不再使用的缓存;同时保留回滚所需的上一版资源窗口。
追问四:发现坏 worker 怎么止损?
停止新入口发布,必要时部署 no-op worker 或回滚稳定脚本;保留旧资源,修复后再逐步激活。
追问五:如何证明更新安全?
测试多个标签页、离线启动、旧 HTML 加新资源、刷新、更新提示、API 兼容、错误监控和完整回滚。