Service Worker 我用它做过离线缓存:拦截请求容易,难的是缓存策略、更新时机和后台同步这些边界。
Q1: Service Worker 的历史?为什么要有它?
1
Web 早期: 网络是生命线
传统 Web 应用完全依赖网络——断网就是白屏。HTTP 缓存(Cache-Control、ETag 等)只能优化重复请求,无法让应用在离线状态下工作。浏览器能做的只是缓存静态资源,但”缓存”和”离线可用”是两回事。
2
移动时代: Web 体验的致命短板
2012 年前后,移动互联网爆发。用户习惯了原生 App 的”随时可用、推送通知”体验,而 Web 应用做不到——没有网络就打不开,没有推送就唤不回用户。Google 意识到 Web 必须进化。
3
2014-2015 年: Service Worker 与 PWA
2014 年 Service Worker 规范草案发布,Chrome 40(2015 年)开始正式支持。Google 在 2015 年提出 PWA(Progressive Web App)概念,Service Worker 是其核心技术。它是一个运行在浏览器后台的独立线程,可以拦截网络请求、管理缓存、接收推送通知——即使页面关闭,Service Worker 也能被浏览器重新启动。这从根本上解决了 Web 应用的离线问题。
4
现代: 离线优先成为标准
现代浏览器全面支持 Service Worker。大型应用(如 Twitter Lite、Starbucks PWA)已经证明了 PWA 的商业价值——安装包小、加载快、可离线使用。Service Worker 让 Web 应用第一次在体验上接近了原生 App。
Service Worker 是浏览器后台的”网络代理”,让 Web 应用实现了离线缓存、推送通知等原生 App 级别的能力。
🚫 没有 Service Worker
Web 应用就像一个只能在有 WiFi 的房间里才能打开的冰箱——断网了,门都打不开。用户每次访问都要重新下载所有资源,浪费流量、浪费时间。
✅ 引入 Service Worker
有了 Service Worker,Web 应用变成了一个自带食物的冰箱——即使断电(断网),里面的食物(缓存数据)仍然可以使用。而且它还能在后台帮你接收快递(推送通知),即使你不在家(页面关闭)。
Q2: Service Worker 的生命周期(install、activate、fetch)
Service Worker 的本质是一个运行在浏览器后台的 JavaScript 线程,它独立于页面,有自己的生命周期。理解这个生命周期是掌握 Service Worker 的关键。页面如何注册 Service Worker
Service Worker 的作用域
Service Worker 的作用域由其文件路径决定。/sw.js 的默认作用域是 /,/assets/sw.js 的默认作用域是 /assets/。可以通过注册时的 scope 选项修改,但不能超出文件路径的范围。
Q3: Cache API 详解(缓存策略、离线支持)
Cache API 是与 Service Worker 配合的核心 API,它提供了一个独立于 HTTP 缓存的键值对存储,可以缓存 Request/Response 对象。Cache API 的基本操作
五种经典缓存策略
Q4: Service Worker 的实际应用(PWA、离线缓存、推送通知)
应用一:PWA 离线应用
应用二:推送通知
应用三:后台同步
应用四:缓存版本管理
Q5: Service Worker 的限制与安全
限制一:只能运行在 HTTPS 上
Service Worker 只能在安全上下文(Secure Context)中运行——即 HTTPS 或 localhost。这是因为 Service Worker 拦截的是网络请求,如果通过 HTTP 传输,中间人可以篡改 Service Worker 脚本,从而控制整个应用的所有网络请求。限制二:无法访问 DOM
Service Worker 运行在独立的 worker 线程中,无法直接访问 DOM。它与页面的通信只能通过postMessage API。
限制三:无同步 XHR
Service Worker 中不能使用同步 XMLHttpRequest。所有网络请求必须是异步的(使用 Fetch API)。这是为了保证 Service Worker 不会阻塞主线程。限制四:不能访问 localStorage
Service Worker 无法使用localStorage(因为 localStorage 与 window 绑定)。可以使用 IndexedDB 或 Cache API 作为替代。
限制五:浏览器兼容性与生命周期管理
Service Worker 在某些场景下会被浏览器终止以节省内存(尤其是移动端)。虽然浏览器会在需要时重新启动它,但开发者不能假设 Service Worker 一直在运行。所有状态必须持久化到 IndexedDB 或 Cache API 中。Q6: 经典面试题:Service Worker 和 Web Worker 的区别?
❌/✅ 代码对比(通信方式):
底层源码:Chromium 是如何实现 Service Worker 的?
Chromium 的 Service Worker 是怎么实现的?
Chromium 的 Service Worker 实现在 content/browser/service_worker/ 目录下。核心类是 ServiceWorkerVersion(详见 Chromium 源码:content/browser/service_worker/service_worker_version.h):
Service Worker 的 fetch 拦截是怎么实现的?
Fetch 拦截在 Blink 渲染引擎的 ServiceWorkerGlobalScope 中实现(详见 Chromium 源码:third_party/blink/renderer/modules/service_worker/):
及格回答 vs 满分回答
问题:“说说 Service Worker 的生命周期”
及格回答(60-70分):“Service Worker 有三个生命周期:install 缓存资源,activate 清理旧缓存,fetch 拦截请求。”问题出在:知道三个事件但不理解激活等待机制。 满分回答(90-100分):
Service Worker 的生命周期包含注册(register)、安装(install)、激活(activate)和运行(fetch)四个阶段。注册由页面 JS 触发,浏览器在后台下载并解析 Service Worker 脚本。install 事件用于预缓存关键静态资源——通过caches.open()和cache.addAll()将资源存入 Cache API。event.waitUntil()可以延长 install 阶段,确保所有资源缓存完成。[1]
关键的细节是激活等待机制:新版本 Service Worker 安装完成后不会立刻激活,它会等到所有使用旧版本的页面都关闭后才进入 activate 阶段。这是为了防止新旧版本同时控制页面导致行为不一致。如果需要跳过等待,可以在 install 中调用self.skipWaiting(),在 activate 中调用self.clients.claim()立即接管所有页面。[2]
activate 事件的核心任务是清理旧缓存——通过 caches.keys() 获取所有缓存名称,删除不在白名单中的旧缓存。这保证了用户的存储空间不会被无限占用。
实际项目中需要注意:浏览器只在特定时机检查更新——用户导航到作用域内的页面、调用navigator.serviceWorker.register(),或浏览器自身的定期检查(规范建议约 24 小时一次)。因此新版本往往不会在发版后立刻生效。想提示用户”有新版本可用”,应该在registration.onupdatefound或新 Worker 进入waiting状态时通过postMessage通知,而不是等activate之后——那时新版本已经在接管了。
常见追问
Service Worker 和 Web Worker 的区别?
Service Worker 和 Web Worker 的区别?
答题要点:
从生命周期、能力范围、使用场景三个维度对比。高分回答:
“Service Worker 和 Web Worker 都是运行在独立线程中的 JS 脚本,但它们的定位完全不同。
生命周期:Web Worker 依附于页面,页面关闭即终止;Service Worker 独立于页面,可以后台存活(即使页面关闭,SW 仍在运行,直到浏览器决定终止它以节省内存)。
能力范围:Service Worker 可以拦截网络请求、管理缓存、接收推送通知;Web Worker 只能做计算。
使用场景:Service Worker 用于 PWA 离线缓存、推送通知、后台同步;Web Worker 用于计算密集型任务(大数组排序、图片处理)。
Service Worker 的缓存策略有哪些?如何选择?
Service Worker 的缓存策略有哪些?如何选择?
答题要点:
不只是列出策略,要给出选择依据。高分回答:
“五种经典缓存策略:
- Cache First:缓存优先,适用于静态资源(CSS、JS、图片)——这些资源更新频率低,缓存命中率高。
- Network First:网络优先,适用于需要最新数据的页面(新闻、API)——网络失败时降级到缓存。
- Stale While Revalidate:先返回缓存,同时后台更新,适用于频繁更新但允许稍旧的数据(用户头像、评论列表)。
- Cache Only:只走缓存,适用于预缓存的离线资源。
- Network Only:只走网络,适用于不能缓存的请求(登录、支付、实时数据)。
如何更新 Service Worker 的缓存?
如何更新 Service Worker 的缓存?
答题要点:
缓存版本管理 + 激活清理 + 用户通知。高分回答:
“更新缓存的核心是版本管理:浏览器的更新检测:当 SW 文件内容发生字节级变化时,浏览器会触发更新流程。所以很多项目会在构建时给 SW 文件名加 hash。”
Service Worker 如何处理离线场景?
Service Worker 如何处理离线场景?
答题要点:
区分”首次访问”和”二次访问”两种场景。高分回答:
“分两种情况:
1. 首次访问(从未缓存过):Service Worker 需要在 install 阶段预缓存关键资源。如果首次访问时就断网,预缓存会失败——所以 PWA 的首次加载仍然需要网络。
2. 二次访问(已有缓存):Service Worker 可以从 Cache API 返回缓存的响应,即使网络不可用。这就是离线能力的核心。关键点:要准备一个 offline.html 作为兜底页面,当请求和缓存都没有时返回给用户。”
如何用 Service Worker 实现推送通知?
如何用 Service Worker 实现推送通知?
答题要点:
推送通知需要三方配合:Push API、Notification API、Service Worker。高分回答:
“推送通知需要三个 API 配合:服务器通过 VAPID 协议向浏览器推送端点发送加密消息,浏览器收到后触发 Service Worker 的 push 事件。”
- Push API(
PushManager):订阅推送,获取推送端点(endpoint) - Notification API:显示系统通知
- Service Worker:接收推送事件,显示通知
Service Worker 的作用域是怎么工作的?
Service Worker 的作用域是怎么工作的?
答题要点:
作用域决定了 SW 能控制哪些页面。高分回答:
“Service Worker 的作用域由其文件路径和注册时的 scope 选项共同决定。默认作用域是 SW 文件所在的目录。例如 最佳实践:把 SW 放在根目录,控制整个站点,通过 fetch 事件中的 URL 判断不同的缓存策略。”
/assets/sw.js 的默认作用域是 /assets/。
关键规则:- scope 不能超出 SW 文件的目录范围——
/assets/sw.js不能注册/的作用域 - 一个页面只能被一个 SW 控制——如果多个 SW 的作用域重叠,最具体的(最长路径匹配的)获胜
- 顶层作用域(
/)的 SW 可以控制所有页面
🔥 Service Worker 的更新机制是什么?如何强制更新?
🔥 Service Worker 的更新机制是什么?如何强制更新?
答题要点:
浏览器的更新检测逻辑 + 强制更新方法。高分回答:
“浏览器检测 Service Worker 更新的条件(满足任一即可):注意:
- 用户导航到 SW 作用域内的新页面
- 调用
navigator.serviceWorker.register()(即使路径相同) - 距离上次检查超过 24 小时(浏览器自动检查)
skipWaiting() 会导致新旧版本同时控制页面,可能导致状态不一致。建议在 activate 后通知用户刷新页面。”🔥 如何用 Service Worker 实现后台同步?
🔥 如何用 Service Worker 实现后台同步?
答题要点:
Background Sync API 的使用场景和限制。高分回答:
“后台同步(Background Sync)允许 Service Worker 在网络恢复时自动发送离线期间积累的数据。典型场景:离线表单提交、离线消息发送。限制:后台同步不保证精确时间——浏览器会根据电量、网络状况等决定何时触发。不要用它做定时任务。“
延伸阅读与引用参考
- 🔗 MDN Service Worker API:Service Worker 的完整 API 文档和生命周期说明。[1]
- 📖 The Offline Cookbook:Jake Archibald 的离线缓存策略圣经,详细介绍了五种缓存策略的实现。[2]
- 🟢 Chromium Service Worker 源码:Chromium 中 Service Worker 的内部实现,包括生命周期管理和 fetch 拦截逻辑。[3]
