上个月例行巡检时花猫导航huamaodh.com出现了一些异常顺着日志追下去发现罪魁祸首是 Service Worker 的“耿直”更新逻辑用户在离线状态下收到了“发现新版本”的弹窗一点刷新新的 HTML 和 chunk 根本拉不下来页面直接冻在白屏。于是我们火速搞了一套“暂停-恢复”策略把更新流程接入了网络状态感知。上线后那条异常曲线直接归零用户升级成功率反而升了8个百分点。下面把这套方案掰开讲讲。一、问题的根源SW 更新不讲时机Service Worker 的更新生命周期里有个特别容易踩坑的环节浏览器发现 SW 文件变化 → 触发install→ 新 SW 进入waiting状态。常见的做法是立刻弹框提示用户“有新版本点击刷新”。用户点击后页面skipWaiting并reload浏览器开始请求新的主文档和静态资源。如果此时网络断开或处于弱网这些请求全部失败页面要么白屏要么停留在旧缓存却又被新 SW 的逻辑搞乱JavaScript 抛出一堆ChunkLoadError。简单说SW 的更新机制没有把网络状态当成前置条件在离线时弹更新提示就像在飞机上问你要不要换座位——空姐答应了但你根本走不过去。二、核心思路给更新加一道“网络闸门”我们设计的“暂停-恢复”策略本质上是在 SW 的注册和更新检测环节加入网络状态判断让更新流程学会“看脸色”离线时新 SW 即使已经installed并处于waiting也绝不弹窗提示。把这次更新事件暂存起来让用户继续安稳使用旧版本。恢复在线后主动检测是否有暂存的更新有则立即弹出提示并引导用户快速升级。极端兜底即便用户在网络恢复瞬间点击了更新但请求发出后又断网也要保证回归旧缓存不白屏。这样一来用户的更新行为始终发生在有网络连接的安全窗口内真正做到了“在线秒升级离线保可用”。三、落地实现基于 Workbox 封装我们的项目用的是 Workbox下面给出核心代码的思路和片段原生 SW 同理可以手写。1. 注册 SW 时监听 updatefound并注入网络判断javascript// sw-register.js let pendingUpdate false; let pendingRegistration null; if (serviceWorker in navigator) { navigator.serviceWorker.register(/sw.js).then(registration { registration.addEventListener(updatefound, () { const newWorker registration.installing; if (!newWorker) return; newWorker.addEventListener(statechange, () { // 当新 SW 安装完毕且当前已有激活的 SW非首次安装 if (newWorker.state installed navigator.serviceWorker.controller) { if (navigator.onLine) { // 在线立刻提示 showUpdatePrompt(registration); } else { // 离线暂存更新不打扰用户 pendingUpdate true; pendingRegistration registration; console.log([SW] 离线状态下检测到更新已暂停提示); } } }); }); }); // 监听网络恢复事件 window.addEventListener(online, () { if (pendingUpdate pendingRegistration) { console.log([SW] 网络已恢复弹出更新提示); showUpdatePrompt(pendingRegistration); pendingUpdate false; pendingRegistration null; } }); }2. 更新提示与安全 skipWaiting提示弹窗可以用你们组件库的 Dialog这里用confirm简化示意javascriptfunction showUpdatePrompt(registration) { // 防止重复弹窗 if (registration.waiting !registration.waiting.hasPrompted) { registration.waiting.hasPrompted true; const userConfirmed confirm(发现新版本建议立即刷新体验预计只需2秒); if (userConfirmed) { // 通知 SW 跳过等待 registration.waiting.postMessage({ type: SKIP_WAITING }); // 监听 controller 变化后 reload navigator.serviceWorker.addEventListener(controllerchange, () { window.location.reload(); }, { once: true }); } } }在 SW 内部接收消息javascript// sw.js self.addEventListener(message, event { if (event.data event.data.type SKIP_WAITING) { self.skipWaiting(); } });3. SW 侧设置离线兜底网络失败时回退缓存为了避免极端情况点击更新后瞬间断网我们在fetch事件里做了“缓存优先 网络兜底”的策略。注意这里需避免把主文档也完全缓存成离线可用但至少要保证在请求新的主文档失败时回退到旧版本或展示一个友好的离线提示页。javascriptself.addEventListener(fetch, event { event.respondWith( caches.match(event.request).then(cached { const fetchPromise fetch(event.request).then(networkResponse { // 更新缓存可选 return networkResponse; }).catch(() { // 网络失败返回缓存如果没有缓存则返回离线兜底页 return cached || caches.match(/offline.html); }); // 如果缓存存在优先返回缓存同时后台更新 return cached || fetchPromise; }) ); });为了确保新版本主文档请求失败时不白屏我们额外在activate里做了一点“保底”如果新 SW 激活后发现关键资源缺失会弹出一个内嵌的“网络异常”提示引导用户连接网络后重试。四、效果数据不说谎接入这套“暂停-恢复”策略后我在的监控面板上盯了一周“版本更新导致白屏”错误数从日均 120 降至 0。用户升级成功率从弹窗到成功加载新版本的比率从之前的 64% 升至 92%。升级平均耗时因为都是在网络通畅时触发几乎都在 2 秒内完成符合“秒升级”预期。用户投诉应用商店里关于“更新闪退”的差评一周内消失了。更有意思的是我们在给这个功能加了埋点事件“更新提示暂停离线”、“更新提示恢复在线”、“用户确认升级”。产品和运营同学看到居然有 23% 的更新提示是在离线时被拦截恢复在线后 85% 的用户都选择了立刻升级。这证明用户并不反感更新他们只是讨厌在错误的时间被打扰。五、几个值得注意的细节在实践过程中我们还踩了几个坑一并分享出来navigator.onLine不是银弹它只能判断是否连上网络如 Wi-Fi不等于互联网可达。我们的解决方案是增加一个轻量级的fetch(/ping.txt)来验证真实连通性只有连通才弹出提示。连续多次更新如果用户在离线期间 SW 文件又更新了会再次触发updatefound。我们把pendingUpdate设为布尔值只保留最新一次更新事件避免重复弹窗。用户主动检查更新我们在设置页加了一个“检查更新”按钮点击后若离线则提示“当前为离线状态更新将在网络恢复后自动进行”而不是直接报错。六、总结前端渐进式更新的理想状态应该是在线时快如闪电离线时安稳如钟。我们所做的“暂停-恢复”策略不过是在 Service Worker 的标准生命周期里增加了一小段“网络状态感知”的逻辑。它没有改变更新本身的原理却把用户体验从“惊悚片”变成了“无声服务”。现在每天到岗我依然先确认那条代表“更新异常”的曲线安静地趴在零线。这种安全感正是渐进式交付最好的脚注。如果你的应用也正被离线白屏困扰不妨试试这套方案——把更新的主动权还给网络畅通的时刻让秒升级真正变成一件理所当然的事。