学完这篇,你能给自己的网页加上 Service Worker,做到断网也能打开、静态资源秒开,并且知道每种缓存策略该用在什么场景。
第一步:确认前提条件
Service Worker 只能在安全上下文里跑,也就是 https:// 或者 http://localhost。用 IP 直连(比如 http://192.168.1.10)在部分浏览器上会被拒绝注册。
注意:Service Worker 作用域(scope)默认是脚本所在目录。把 sw.js 放在根目录,它才能接管全站;放在 /js/sw.js 就只能管 /js/ 下的请求。
目录结构建议这样:
/sw.js
/index.html
/offline.html
/css/style.css
/js/app.js
第二步:在主页面注册
在 index.html 的 </body> 前加:
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('注册成功,范围:', reg.scope))
.catch(err => console.error('注册失败:', err));
});
}
</script>
放在 load 之后注册,避免和首屏资源抢带宽。
第三步:预缓存核心资源
新建 sw.js,先做「安装时缓存」:
const VERSION = 'v1';
const STATIC_CACHE = `static-${VERSION}`;
const RUNTIME_CACHE = `runtime-${VERSION}`;
const PRECACHE = ['/', '/index.html', '/offline.html', '/css/style.css'];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(STATIC_CACHE)
.then(cache => cache.addAll(PRECACHE))
.then(() => self.skipWaiting())
);
});
skipWaiting() 让新版本立即进入激活流程,不用等用户关掉所有标签页。
注意:cache.addAll() 是「全成功才算成功」。列表里任何一个 404,整个安装就会失败,而且报错信息很含糊。先用浏览器逐个打开确认能访问。
第四步:清理旧版本缓存
版本号一改,旧缓存要删掉,否则磁盘里会堆一堆废数据:
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(
keys.filter(k => k !== STATIC_CACHE && k !== RUNTIME_CACHE)
.map(k => caches.delete(k))
)
).then(() => self.clients.claim())
);
});
以后每次发版,只要把 VERSION 改成 v2,旧缓存自动清空。
第五步:按请求类型分配策略
这一步是重点。别用一种策略打天下,导航请求和静态资源的需求完全不同:
self.addEventListener('fetch', event => {
const req = event.request;
if (req.method !== 'GET') return;
const url = new URL(req.url);
if (url.origin !== location.origin) return;
// 策略一:导航请求 —— 网络优先,失败回退缓存,再失败给离线页
if (req.mode === 'navigate') {
event.respondWith(
fetch(req)
.then(res => {
const copy = res.clone();
caches.open(RUNTIME_CACHE).then(c => c.put(req, copy));
return res;
})
.catch(() => caches.match(req).then(r => r || caches.match('/offline.html')))
);
return;
}
// 策略二:静态资源 —— 缓存优先 + 后台静默更新
event.respondWith(
caches.match(req).then(cached => {
const network = fetch(req)
.then(res => {
if (res && res.status === 200 && res.type === 'basic') {
const copy = res.clone();
caches.open(RUNTIME_CACHE).then(c => c.put(req, copy));
}
return res;
})
.catch(() => cached);
return cached || network;
})
);
});
三种常见策略的适用场景,记住这张对照表就够了:
| 策略 | 行为 | 适合 |
|---|
| Cache First | 先查缓存,没有才联网 | CSS/JS/图片/字体 |
| Network First | 先联网,失败读缓存 | HTML 页面、API 列表 |
| Stale-While-Revalidate | 先返回缓存,同时后台更新 | 头像、排行榜这类「旧一点没关系」的数据 |
上面的策略二其实就是 Stale-While-Revalidate 的写法。
第六步:调试与验证
打开 DevTools → Application 面板:
- Service Workers:勾选 Offline 模拟断网,刷新看是否还能打开
- Cache Storage:点开能看到缓存了哪些 URL 和响应状态
- Update on reload:开发时勾上,每次刷新强制更新 SW,省得手动 unregister
注意:普通的「强制刷新(Ctrl+F5)」不会绕过 Service Worker。要彻底清干净,在 Application 面板点 Clear storage。
三个高频坑
- 接口别用 Cache First:用户看到的是登录前的旧数据,还查不出原因。API 请求统一走 Network First,或者干脆
return 不拦截。
- SW 更新有延迟:浏览器默认在导航后 24 小时内不重复检查
sw.js。想主动触发,调 reg.update()。
- 改了代码没生效:99% 是
VERSION 没改。缓存名不变,install 阶段的 addAll 会因为文件已存在而走缓存里的旧文件。
小结
- Service Worker 必须跑在 HTTPS 或 localhost,脚本位置决定作用域
install 预缓存核心文件,activate 清理旧版本缓存,靠 VERSION 常量驱动
- 导航请求用 Network First + 离线兜底页,静态资源用 Cache First 或 Stale-While-Revalidate
- 只处理 GET 同源请求,POST 和跨域接口不要插手
- 调试去 Application 面板,用 Clear storage 彻底重置