Service-Worker与离线缓存
1. 概念
Service Worker(SW)是浏览器后台脚本,能拦截网络请求,做缓存、离线支持、推送通知。前端进阶:PWA。
浏览器请求
↓
[Service Worker] ← 拦截
↓ ↑
缓存 / 网络
SW 注册后首次访问仍走网络,第二次起 SW 才能拦截。
2. 注册
// main.ts
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const reg = await navigator.serviceWorker.register('/sw.js', { scope: '/' })
console.log('SW registered:', reg.scope)
} catch (err) {
console.error('SW registration failed:', err)
}
})
}
3. 生命周期
register → install → waiting → activate → fetch
↓ ↓
(skipWaiting) (clients.claim)
// sw.js
const CACHE = 'v1'
self.addEventListener('install', (e) => {
e.waitUntil(
caches.open(CACHE).then(c => c.addAll([
'/',
'/index.html',
'/assets/app.css',
'/assets/app.js',
]))
)
self.skipWaiting() // 立即激活,不等老 SW 关闭
})
self.addEventListener('activate', (e) => {
e.waitUntil(
caches.keys().then(keys => Promise.all(
keys.filter(k => k !== CACHE).map(k => caches.delete(k))
))
)
self.clients.claim() // 立即接管现有页面
})
4. 缓存策略
4.1 Cache First(静态资源)
self.addEventListener('fetch', (e) => {
if (e.request.url.match(/\.(js|css|woff2|png)$/)) {
e.respondWith(
caches.match(e.request).then(res => res || fetch(e.request).then(r => {
const clone = r.clone()
caches.open(CACHE).then(c => c.put(e.request, clone))
return r
}))
)
}
})
4.2 Network First(API)
if (e.request.url.includes('/api/')) {
e.respondWith(
fetch(e.request).then(r => {
const clone = r.clone()
caches.open(CACHE).then(c => c.put(e.request, clone))
return r
}).catch(() => caches.match(e.request)) // 网络挂用缓存
)
}
4.3 Stale While Revalidate(HTML)
e.respondWith(
caches.match(e.request).then(cached => {
const fetchPromise = fetch(e.request).then(r => {
caches.open(CACHE).then(c => c.put(e.request, r.clone()))
return r
})
return cached || fetchPromise
})
)
返回缓存(快),后台更新(最终新鲜)。
5. workbox(推荐)
Google 出的 SW 工具库,常见模式封装:
// vite.config.ts
import { VitePWA } from 'vite-plugin-pwa'
export default {
plugins: [
VitePWA({
strategies: 'generateSW', // 或 injectManifest(自定义)
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,png,svg,woff2}'],
runtimeCaching: [{
urlPattern: /^https:\/\/fonts\.gstatic\.com/,
handler: 'CacheFirst',
options: {
cacheName: 'fonts',
expiration: { maxEntries: 30, maxAgeSeconds: 365 * 86400 },
}
}, {
urlPattern: /\/api\//,
handler: 'NetworkFirst',
options: {
cacheName: 'api',
networkTimeoutSeconds: 5,
expiration: { maxAgeSeconds: 300 },
}
}]
}
})
]
}
6. 更新策略
SW 缓存了旧版本 = 用户拿不到新版。三种更新模式:
6.1 立即更新(skipWaiting + clients.claim)
新 SW 一注册立即接管。可能造成"新 JS + 旧 HTML"短暂混搭。
6.2 等待用户下次访问
不 skipWaiting。新 SW 等到所有页面关闭后才激活。安全但更新慢。
6.3 提示用户刷新(推荐)
// workbox-window
import { Workbox } from 'workbox-window'
const wb = new Workbox('/sw.js')
wb.addEventListener('waiting', () => {
if (confirm('新版本可用,立即刷新?')) {
wb.messageSkipWaiting()
wb.addEventListener('controlling', () => location.reload())
}
})
wb.register()
7. 部署注意
7.1 sw.js 不能缓存
location = /sw.js {
add_header Cache-Control "no-cache, no-store, must-revalidate";
expires 0;
try_files $uri =404;
}
否则用户永远拿到旧 SW = 永远不更新。
7.2 sw.js 必须从根域
scope 默认是 sw.js 所在路径。/sw.js scope = /,/static/sw.js scope = /static/。前端 SPA 必须在根。
7.3 必须 HTTPS
SW 只在 HTTPS(或 localhost)可用。
8. 离线支持
self.addEventListener('fetch', (e) => {
e.respondWith(
fetch(e.request).catch(() => {
// 网络失败 + 没缓存 → fallback 页面
if (e.request.mode === 'navigate') {
return caches.match('/offline.html')
}
})
)
})
PWA 必备。
9. 调试
Chrome DevTools → Application → Service Workers:
- 看注册状态
- "Update on reload" 开发时强制每次更新 SW
- "Bypass for network" 暂时关 SW
- "Unregister" 卸载 SW
清缓存:Application → Storage → Clear site data。
10. 常见反模式
- sw.js 长缓存:用户永远拿旧 SW
- 缓存所有 API:私有数据被串
- 没有版本号 / cleanup:旧缓存堆积
- skipWaiting 不通知用户:新旧 JS 混搭报错
- 不处理离线:网络挂掉白屏
- cache 没限大小:浏览器自动清,结果丢关键资源
- SW 拦截 SSE / WebSocket:长连接被破坏