Service Worker 的生命周期、离线缓存和更新机制是怎样的?
简化版
Service Worker 是运行在页面之外的后台脚本,像浏览器、页面和网络之间的可编程代理,可以拦截请求、管理缓存、支持离线访问、推送和后台同步。它有注册、安装、激活、控制页面、更新等生命周期,常见坑是缓存版本管理和新旧 SW 并存。
详细版
基本流程:
register -> install -> activate -> fetch events -> update
常见用途:
- 静态资源预缓存。
- 请求拦截和运行时缓存。
- 离线兜底页面。
- PWA 安装体验。
- Push 和 Background Sync 等能力。
Service Worker 不能直接操作 DOM,和页面通过 postMessage 通信。它通常要求 HTTPS 环境。更新时,新 worker 安装后可能处于 waiting,直到旧页面关闭或主动 skipWaiting 和 clients.claim 才接管。
完整版教学
一、Service Worker 是网络代理,不是普通 Worker
Service Worker 运行在页面之外,可以在页面和网络之间拦截请求。它不是用来做普通 CPU 计算的 Worker,而是偏向缓存、离线、推送和后台能力。
页面 fetch('/app.js')
-> Service Worker fetch 事件
-> Cache Storage 或 Network
-> 返回 Response
它能让网页在弱网、断网或重复访问时更稳定。比如第一次访问缓存了核心 JS/CSS,第二次即使网络慢,也能先从缓存返回应用外壳。
面试时要把 Web Worker 和 Service Worker 区分开:前者偏后台计算,后者偏网络拦截和离线能力。
二、注册、安装、激活是生命周期主线
页面通过 navigator.serviceWorker.register() 注册 SW 文件。浏览器下载并解析成功后,触发 install;安装完成后进入 activate;激活后才能控制页面并处理 fetch。
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
}
Service Worker 文件:
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('app-v1').then(cache => cache.addAll(['/offline.html']))
)
})
self.addEventListener('activate', (event) => {
event.waitUntil(cleanOldCaches())
})
register -> install -> waiting -> activate -> controlling clients
install 适合预缓存核心资源,activate 适合清理旧缓存和迁移状态。不要把大量不可控请求都塞进 install,否则安装失败会导致 SW 不可用。
三、fetch 事件决定缓存策略
Service Worker 的核心能力是监听 fetch。不同资源适合不同缓存策略:HTML 通常要网络优先或短缓存,带 hash 的静态资源可以缓存优先,接口数据要按业务处理。
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(cached => {
return cached || fetch(event.request)
})
)
})
| 策略 | 适合资源 | 风险 |
|---|---|---|
| Cache First | hash 静态资源、字体、图片 | 旧资源清理 |
| Network First | HTML、关键接口 | 弱网等待较久 |
| Stale While Revalidate | 列表、非关键资源 | 短暂看到旧数据 |
| Offline Fallback | 离线页面 | 内容可能不完整 |
数字例子:一个 300KB 的主 JS 使用 hash 文件名,可以长期 Cache First;而 /index.html 如果长期缓存,用户可能一直加载旧资源入口。
四、更新机制的难点是新旧版本并存
浏览器发现 SW 文件内容变化,会安装新的 Service Worker。但如果旧页面仍打开,新 SW 可能进入 waiting 状态,不会立刻控制页面。
旧 SW 控制页面 A
新 SW 下载并 install
-> waiting
页面 A 关闭或主动跳过等待
-> 新 SW activate
这能避免一个页面运行期间资源版本突然混用。但工程上也会带来“发布了用户没立即更新”的体验。
可以使用 self.skipWaiting() 让新 SW 尽快激活,再用 clients.claim() 接管页面。但这可能导致当前页面 JS 和缓存资源版本不匹配,所以要谨慎。
易错点:Service Worker 更新不是刷新 CDN 那么简单,页面、SW、Cache Storage 三者都可能处在不同版本。
五、缓存版本管理是上线稳定性的关键
SW 常用缓存名带版本,例如 app-v1、app-v2。新版本激活时清理旧缓存,避免资源无限堆积和旧文件误用。
const CACHE_NAME = 'app-v2'
async function cleanOldCaches() {
const keys = await caches.keys()
return Promise.all(
keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key))
)
}
如果每次发布都新增 5MB 缓存,10 次发布就是 50MB。移动端存储紧张时,浏览器可能自动清理缓存,用户离线体验就变得不可预测。
同时要避免缓存不透明。线上问题排查时,用户可能不是请求到了服务器旧文件,而是 SW 返回了旧缓存。排查要看 DevTools 的 Application 面板和 Network 是否来自 Service Worker。
六、安全边界和适用场景
Service Worker 通常要求安全上下文,也就是 HTTPS 或本地开发环境。因为它能拦截请求,如果被中间人篡改,危害很大。
如果 SW 被恶意替换:
-> 可伪造离线内容
-> 可缓存恶意资源
-> 可劫持同作用域请求
作用域也很重要。注册在 /sw.js 的 SW 通常能控制根路径下页面;注册在 /app/sw.js 的作用域更窄。作用域越大,影响越大,设计时要明确。
适合使用 SW 的场景包括文档站、后台系统应用壳、弱网业务、PWA、离线阅读、静态资源缓存。不适合对实时性强且缓存策略复杂的核心交易数据无脑缓存。
七、常见误区与追问
- 误区:Service Worker 可以直接操作 DOM。 它运行在页面外,不能直接访问 DOM,需要通过消息与页面通信。
- 误区:发布新 SW 后用户立刻使用新版本。 新 SW 可能 waiting,直到旧页面关闭或主动跳过等待。
- 误区:所有请求都 Cache First 最快。 HTML 和接口无脑缓存会造成旧页面、旧数据和业务错误。
- 追问:skipWaiting 有什么风险? 新 SW 立即接管可能让当前页面和缓存资源版本不匹配。
- 追问:如何排查 SW 缓存问题? 看 DevTools Application、Cache Storage、Network 中是否 from ServiceWorker,并尝试 unregister。
- 追问:SW 为什么要求 HTTPS? 它能拦截请求,必须防止传输过程中被篡改。
八、加强记忆
Service Worker 按“代理、生命周期、策略、更新”记:它是页面和网络之间的代理;生命周期有 install、activate、fetch;缓存策略要按资源分类;更新时新旧版本会并存。把 Cache Storage 和 SW 版本一起管理,离线能力才可靠。