跳到主要内容

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:长连接被破坏

11. 延伸阅读