Service Worker 与 PWA 入门:给网页加上离线能力
地铁里刷技术文章,进隧道断网,页面直接白屏——这种体验在 2022 年已经不该存在了。这周给自己的小项目接了 Service Worker,断网也能照常浏览。把实现过程整理成一篇入门指南。
Service Worker 是什么
一句话:跑在独立线程里的可编程网络代理。
它的关键特性:
- 独立于页面的线程,页面关了它还在
- 能拦截页面发出的所有网络请求,决定给缓存还是给网络
- 只能跑在 HTTPS 下(localhost 例外)
- 生命周期特殊:install → activate → fetch,不随页面刷新重来
有了"拦截请求"这个能力,离线缓存、资源预载、后台同步都成了可能。
最小实现:三步走
第一步:注册(页面主线程)
// main.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then((reg) => console.log('SW 注册成功,范围:', reg.scope))
.catch((err) => console.error('SW 注册失败:', err))
})
}
第二步:安装时预缓存(sw.js)
const CACHE_NAME = 'app-v1'
const PRECACHE = ['/', '/index.html', '/app.css', '/app.js', '/logo.png']
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE))
)
self.skipWaiting() // 立即激活,不等旧 SW 控制的页面关闭
})
第三步:拦截请求,缓存优先(sw.js)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request)
})
)
})
这三段跑起来,第二次访问就是秒开,断网也能打开首页。
缓存策略:不是只有"缓存优先"
不同资源该用不同策略,这是 SW 缓存的核心学问。
Cache First(缓存优先):上面写的那个。适合带 hash 的静态资源(app.a3f2.js),内容永不变,缓存命中直接用。
Network First(网络优先):适合API 数据,新鲜度优先,断网才退回缓存:
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
event.respondWith(
fetch(event.request)
.then((response) => {
// 网络成功,顺手更新缓存
const clone = response.clone()
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone))
return response
})
.catch(() => caches.match(event.request)) // 断网用缓存兜底
)
}
})
Stale-While-Revalidate(先给缓存,后台更新):体验最好的策略,适合文章、图片这类内容资源:
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
const fetchPromise = fetch(event.request).then((response) => {
const clone = response.clone()
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone))
return response
})
// 有缓存先返回缓存,同时后台发请求更新缓存
return cached || fetchPromise
})
)
})
用户永远秒开,下次访问看到的就是新内容。取舍是可能看到旧版内容,别用在强实时数据上。
版本更新:activate 时清旧缓存
发了新版本(CACHE_NAME 从 app-v1 改 app-v2),老缓存要清掉:
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((names) =>
Promise.all(
names
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
)
)
)
})
更新机制的经典坑:新 sw.js 下载后会进入 waiting 状态,默认要等所有受控页面关闭才激活。所以 install 里那句 self.skipWaiting() 很关键;更完整的方案是配合页面提示"有新版本,点击刷新"。
成为完整 PWA 还差什么
SW 解决离线,PWA 还差一点仪式感:
{
"name": "我的博客",
"short_name": "博客",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#409eff",
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}
加上这个 manifest.webmanifest 并在 HTML 里 link,Android Chrome 就会提示"添加到主屏幕",点开是全屏无地址栏的类原生体验。
调试:Application 面板
DevTools → Application → Service Workers,可以:
- 查看 SW 状态(installing / waiting / active)
- 勾选 Offline 模拟断网
- 勾选 Update on reload 每次刷新更新 SW(开发必勾,不然被旧 SW 坑死)
- Cache Storage 里查看和清空缓存
开发时的最大教训:写完 SW 记得勾 Update on reload,或者干脆无痕窗口测。
小结
- Service Worker 是可编程网络代理,HTTPS 限定,独立于页面线程
- 三步走:注册 → install 预缓存 → fetch 拦截
- 缓存策略按资源选:静态资源 Cache First、API Network First、内容 SWR
- 版本更新靠 CACHE_NAME 改名 + activate 清旧 + skipWaiting
- 开发必勾 Update on reload
接入 SW 后我的小站 Lighthouse PWA 项全绿,断网体验直接拉满。这个 API 的想象空间还很大——后台同步、推送通知都是它的延伸,后面慢慢玩。
看到这里了?如果这篇文章帮到过你
请我喝瓶可乐就好 🥤

评论0 条
还没有评论,来抢沙发~