Service Worker 是 PWA 的核心:拦截网络请求,按策略返回缓存 / 网络 / 兜底页面,
让网页在无网时也能用。下面写一个最简单但完整的离线缓存层。
1. 注册
// main.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js', { scope: '/' })
.then(reg => console.log('SW registered:', reg.scope))
.catch(err => console.error('SW failed:', err))
})
}
scope: '/' 让 SW 拦截整个站点。SW 文件必须从 同源 root 路径 提供
(不能放 CDN)。
2. SW 主体
// public/sw.js
const VERSION = 'v3-2026-05-23'
const PRECACHE = `precache-${VERSION}`
const RUNTIME = `runtime-${VERSION}`
const PRECACHE_URLS = [
'/',
'/index.html',
'/assets/main.css',
'/assets/main.js',
'/offline.html',
]
// 安装:预缓存 shell 资源
self.addEventListener('install', event => {
event.waitUntil(
caches.open(PRECACHE)
.then(cache => cache.addAll(PRECACHE_URLS))
.then(() => self.skipWaiting()) // 立刻激活,不等老 SW 退出
)
})
// 激活:清理老版本缓存
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys()
.then(keys => Promise.all(
keys.filter(k => k !== PRECACHE && k !== RUNTIME)
.map(k => caches.delete(k))
))
.then(() => self.clients.claim())
)
})
// fetch:按策略响应
self.addEventListener('fetch', event => {
const { request } = event
if (request.method !== 'GET') return // 只缓存 GET
const url = new URL(request.url)
if (url.origin !== self.location.origin) return // 不管跨域
// 1. 导航请求 → 网络优先,失败回退到 offline.html
if (request.mode === 'navigate') {
event.respondWith(
fetch(request).catch(() => caches.match('/offline.html'))
)
return
}
// 2. 静态资源(hash 文件名)→ 缓存优先(永久)
if (url.pathname.match(/\.(css|js|woff2|png|svg|ico)$/)) {
event.respondWith(
caches.match(request).then(cached => {
if (cached) return cached
return fetch(request).then(response => {
if (response.ok) {
const clone = response.clone()
caches.open(RUNTIME).then(c => c.put(request, clone))
}
return response
})
})
)
return
}
// 3. API 请求 → 网络优先,失败用缓存
if (url.pathname.startsWith('/api/')) {
event.respondWith(
fetch(request).then(response => {
if (response.ok) {
const clone = response.clone()
caches.open(RUNTIME).then(c => c.put(request, clone))
}
return response
}).catch(() => caches.match(request))
)
return
}
})
3. offline 页
<!-- public/offline.html -->
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>离线</title></head>
<body>
<h1>当前离线</h1>
<p>请检查网络后重试。已缓存的页面仍然可以打开。</p>
</body>
</html>
4. 三种缓存策略速查
| 策略 | 适用 | 实现 |
|---|---|---|
| Cache-first | 静态资源(hash 文件名)、字体 | match → 否则 fetch + cache |
| Network-first | HTML / API(要新鲜数据) | fetch → 失败用 cache |
| Stale-while-revalidate | 头像、缩略图(旧的也能用) | match 立刻返回 + 异步 fetch 更新 |
实现 stale-while-revalidate:
caches.match(request).then(cached => {
const fetched = fetch(request).then(response => {
if (response.ok) {
caches.open(RUNTIME).then(c => c.put(request, response.clone()))
}
return response
})
return cached || fetched // 有 cache 立刻给,同时后台更新
})
5. Manifest(让浏览器认你是 PWA)
// public/manifest.json
{
"name": "My App",
"short_name": "MyApp",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#2563eb",
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#2563eb">
满足 PWA 安装条件(SW + manifest + HTTPS + 图标),Chrome 地址栏会出现
"安装应用"图标。
6. 版本更新与"重启提示"
// main.js
navigator.serviceWorker.register('/sw.js').then(reg => {
reg.addEventListener('updatefound', () => {
const nw = reg.installing
nw.addEventListener('statechange', () => {
if (nw.state === 'installed' && navigator.serviceWorker.controller) {
// 新版本可用
if (confirm('有新版本,立即刷新?')) {
window.location.reload()
}
}
})
})
})
7. 调试
Chrome DevTools → Application → Service Workers:
- "Update on reload" 勾上,每次 F5 强制 SW 重新安装
- "Bypass for network" 临时禁用 SW
- "Unregister" 卸载(开发期常用)
Cache 内容在同面板的 Cache Storage 看。
8. Workbox
手写 SW 没问题,但生态成熟后用 Google 的 Workbox:
npm i -D vite-plugin-pwa workbox-window
vite-plugin-pwa 配几行就生成 SW + manifest + precache,并处理"新版本提示"
的边界。生产推荐。
踩过的坑
- SW 必须 HTTPS(localhost 例外)。HTTP 网站永远 register 不上。
- 改了 SW 文件浏览器要等"老 SW 没有 client" 时才激活新版;测试期间
关掉整个 tab 才生效,反复测试很烦——用self.skipWaiting()+
self.clients.claim()强制立刻接管。 - SW 缓存了一个 redirect 响应后,整个站点行为奇怪。
Cache.put()
不要存response.redirected的 response。 - 缓存条目 LRU 上限是浏览器决定的(Chrome 总配额几百 MB-几 GB);
超额时按时间删除最老条目,缓存可能突然失效。
登录后参与评论。