在线秒升级:前端渐进式更新的“暂停-恢复”策略
上个月例行巡检时,花猫导航(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简化示意:
javascript
function 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事件里做了“缓存优先 + 网络兜底”的策略。注意,这里需避免把主文档也完全缓存成离线可用,但至少要保证在请求新的主文档失败时,回退到旧版本或展示一个友好的离线提示页。
javascript
self.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 的标准生命周期里,增加了一小段“网络状态感知”的逻辑。它没有改变更新本身的原理,却把用户体验从“惊悚片”变成了“无声服务”。
现在每天到岗,我依然先确认那条代表“更新异常”的曲线安静地趴在零线。这种安全感,正是渐进式交付最好的脚注。如果你的应用也正被离线白屏困扰,不妨试试这套方案——把更新的主动权还给网络畅通的时刻,让秒升级真正变成一件理所当然的事。
