5月31日 20:28

Service Worker 生命周期怎么跑?注册、安装和激活有哪些坑?

Service Worker 是浏览器放在页面和网络之间的一层后台脚本。它不属于某个页面,也不能直接操作 DOM,但可以拦截请求、管理缓存、处理推送和后台同步。PWA 里的离线访问、秒开体验、资源更新,大多都绕不开它。理解 Service Worker 的关键不是背 API,而是搞清楚它什么时候安装、什么时候激活、什么时候真正开始控制页面。

生命周期怎么走

页面先注册 Service Worker。注册成功不代表它马上接管当前页面,只是告诉浏览器这个作用域下有一个后台脚本。脚本下载后进入 install 阶段,通常在这里预缓存应用壳、离线页和必要静态资源。

javascript
if ('serviceWorker' in navigator) { window.addEventListener('load', async () => { try { const reg = await navigator.serviceWorker.register('/sw.js', { scope: '/' }); console.log('service worker scope:', reg.scope); } catch (err) { console.error('register failed', err); } }); }

install 成功后,新 Worker 默认会等待旧 Worker 退出。所有旧页面关闭后,它才进入 activate。activate 阶段适合清理旧缓存、迁移数据、准备接管页面。真正激活后,它才能处理 fetchpushsync 等事件。

javascript
const CACHE = 'app-v4'; self.addEventListener('install', event => { event.waitUntil(caches.open(CACHE).then(c => c.addAll(['/', '/offline.html']))); }); self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(keys => Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))) ); });

它能做什么

最常见的功能是请求拦截。你可以按资源类型选择缓存策略:静态资源 Cache First,HTML Network First,非强一致接口 Stale While Revalidate。它也可以提供离线页,让网络断开时不至于出现浏览器默认错误页。

Service Worker 还能处理 Web Push 通知和 Background Sync,但这些能力有明显边界。通知需要用户授权,后台同步受浏览器策略和电量优化影响,不能当成一定会准时执行的定时任务。项目里要把它当作增强能力,而不是业务唯一链路。

更新流程为什么要谨慎

Service Worker 更新最怕“半新半旧”。用户打开的页面可能还加载着旧 JS,但新 Worker 已经把缓存换成了新资源,这时点击某个懒加载模块就可能 404。大型前端应用通常会检测到新版本后提示刷新,而不是静默强制切换。缓存清理也要保守,只删确定不用的版本;如果把当前页面依赖的资源删掉,弱网下问题会被放大。还要注意 CDN 缓存,sw.js 本身不能被长时间强缓存,否则你以为发布了新策略,用户浏览器却一直拿着旧脚本。生产环境通常会让 sw.js 使用短缓存或 no-cache,而静态资源用 hash 文件名长期缓存。

常见限制

Service Worker 要求 HTTPS,localhost 例外。它有作用域限制,/app/sw.js 默认只能控制 /app/ 下的页面,如果希望控制全站,通常放在根路径。它不能访问 DOM,和页面通信要用 postMessage。缓存空间也不是无限的,浏览器可能在存储压力大时清理数据。

追问

为什么注册成功后刷新一次才生效?

因为新注册的 Service Worker 默认不会立刻控制已经打开的页面。第一次加载页面时还没有控制者,注册完成后通常要等下一次导航才开始拦截请求。可以用 clients.claim() 让激活后的 Worker 尽快接管页面,但这也可能让当前页面突然使用新缓存策略。取舍是更新速度和稳定性,复杂应用不要盲目强接管。

skipWaiting 和 clients.claim 要不要一起用?

它们可以一起用,但不是所有项目都应该用。skipWaiting() 会让新 Worker 跳过等待,clients.claim() 会让它激活后接管现有页面。好处是更新快,坏处是旧页面的 JS 和新缓存可能混在一起,出现接口或资源版本不匹配。发布包含破坏性变更时,更稳的做法是提示用户刷新。

Service Worker 可以缓存 POST 请求吗?

Cache API 本身主要面向 GET 请求,直接缓存 POST 往往不合适。提交订单、登录、支付这类请求有副作用,缓存会制造非常难排查的问题。离线提交可以用 IndexedDB 暂存数据,等网络恢复后再同步,但要处理幂等、失败重试和冲突。边界是“可延迟的草稿”可以离线,“必须立即确认的交易”不要伪装成成功。

为什么作用域会导致 fetch 不触发?

Service Worker 只能控制自己 scope 范围内的页面。脚本放在 /pwa/sw.js,默认控制不了 /tools 页面,自然也拦截不到那些页面发出的请求。可以通过注册时设置 scope,但前提是服务器允许对应路径。踩坑时先在 DevTools 里看当前页面的 controller,而不是只看注册是否成功。

调试 Service Worker 最该看哪里?

Chrome DevTools 的 Application 面板是第一入口,可以看注册状态、生命周期、缓存和推送事件。Network 面板里如果资源来自 Service Worker,会有明确标识。调试时常见坑是勾选了 Update on reload,却忘了线上没有这个行为。测试发布流程时要用真实构建和普通刷新方式验证,不能只依赖开发模式。

标签:PWA