Service Worker 缓存:离线优先进阶
为什么需要 Service Worker 缓存?
在移动优先、网络环境多变的今天,仅靠浏览器默认的 HTTP 缓存已经无法满足“秒开”、“离线可用”的体验要求。Service Worker 缓存让你以 JavaScript 代码精确控制网络请求的拦截、响应和存储,是实现离线优先、提升性能、节省流量的关键技术。
本教程带你从零掌握 Service Worker 缓存的核心概念、常用策略和工程化实践,真正实现“离线优先进阶”。
什么是 Service Worker?
Service Worker 是独立于网页运行的脚本,充当浏览器与网络之间的代理。它可以拦截页面发出的所有网络请求,根据需要返回缓存内容、发出网络请求或进行自定义处理。其特点包括:
- 基于 HTTPS(本地开发可用 localhost)
- 独立线程,不阻塞 UI
- 事件驱动,激活后可以持久运行
- 拥有自己的存储空间,可访问 Cache API 和 IndexedDB
缓存功能是 Service Worker 最核心的能力,通过 Cache API 实现资源的持久化存储。
缓存的核心 API:Cache Storage
Service Worker 处理缓存主要依赖两个接口:
CacheStorage:全局的caches对象,用来管理多个命名缓存空间(类似目录)。Cache:单个缓存空间,存放Request/Response键值对。
常用方法:
// 打开或创建一个缓存空间
const cache = await caches.open('my-cache-v1');
// 将请求/响应对加入缓存
await cache.add('/app.js'); // 请求并缓存
await cache.addAll(['/app.js', '/style.css']); // 批量添加
await cache.put(request, response); // 手动存入
// 从缓存匹配请求
const cachedResponse = await cache.match(request);
// 全局匹配:在所有缓存空间中查找
const response = await caches.match(request);
// 删除缓存项或整个空间
await cache.delete(request);
await caches.delete('my-cache-v1');
Response 对象只能被读取一次,如果需要同时返回给页面和存入缓存,必须使用 response.clone()。
Service Worker 生命周期与缓存管理
Service Worker 的生命周期直接决定了缓存的最佳操作时机。
1) 安装阶段 (install)
当用户首次访问、或 SW 脚本发生字节变化时,会触发 install 事件。这是预缓存的关键时机,适合缓存应用骨架(App Shell)和关键静态资源。
const PRECACHE = ['/','/css/app.css','/js/app.js'];
self.addEventListener('install', event => {
event.waitUntil(
caches.open('v1.0.0')
.then(cache => cache.addAll(PRECACHE))
.then(() => self.skipWaiting()) // 可选:立即激活
);
});
2) 激活阶段 (activate)
install 完成后 SW 不会立即控制页面,而是进入 waiting 状态,直到旧 SW 释放。activate 事件触发后,可以安全地清理旧版本缓存。
self.addEventListener('activate', event => {
const currentCaches = ['v1.0.0'];
event.waitUntil(
caches.keys()
.then(keys => Promise.all(
keys.filter(key => !currentCaches.includes(key))
.map(key => caches.delete(key))
))
.then(() => self.clients.claim()) // 让新 SW 立即控制所有客户端
);
});
3) 请求拦截阶段 (fetch)
SW 接管页面后,每个网络请求都会触发 fetch 事件,在这里可以自由应用缓存策略。
self.addEventListener('fetch', event => {
// 必须用 event.respondWith 返回你的响应
event.respondWith(/* 自定义策略 */);
});
公共缓存策略详解
理解每种策略的适用场景,是离线进阶的关键。
Cache First(缓存优先)
优先读取缓存,缓存未命中时发起网络请求,适用于不常变的静态资源(CSS、JS 及字体等)。
async function cacheFirst(request) {
const cached = await caches.match(request);
if (cached) return cached;
const response = await fetch(request);
const cache = await caches.open('static');
cache.put(request, response.clone());
return response;
}
Network First(网络优先)
优先尝试网络,网络失败或超时回退到缓存,适合展示最新内容的 API 或 HTML 页面。
async function networkFirst(request) {
const cache = await caches.open('dynamic');
try {
const response = await fetch(request);
cache.put(request, response.clone());
return response;
} catch (err) {
const cached = await cache.match(request);
return cached || caches.match('/offline.html');
}
}
Stale-While-Revalidate(后台更新)
立即返回缓存(如果有),同时在后台发起网络请求更新缓存,下次访问时展示新内容。兼顾速度与新鲜度。
async function staleWhileRevalidate(request) {
const cache = await caches.open('dynamic');
const cached = await cache.match(request);
const fetchPromise = fetch(request).then(response => {
cache.put(request, response.clone());
return response;
});
return cached || fetchPromise;
}
Network Only / Cache Only
不常用,主要用于调试或特定资源策略。
实现离线优先:预缓存关键资源
离线优先的核心是在 install 阶段提前将应用“壳”缓存到设备,使得在无网络时页面仍可完整渲染。
- 明确 App Shell:确定最小化 HTML、CSS、JS、LOGO 等资源构成可用骨架。
- 构建时生成预缓存清单:手动维护清单容易出错,推荐使用 Workbox、webpack 插件自动生成哈希版本化的资源列表。
- 在 install 中执行
cache.addAll,保证这些资源永远能被离线访问。
示例清单生成(使用 Workbox):
import {precacheAndRoute} from 'workbox-precaching';
precacheAndRoute(self.__WB_MANIFEST);
提示:永远不要预缓存过多的动态资源,版本变更会导致频繁下载。
运行时缓存动态内容
对于用户数据、API 结果、CDN 图片等运行时资源,更适合在 fetch 事件中按策略动态缓存。
常见实践:
- API 响应:使用 Network First 或 Stale-While-Revalidate,保证数据准确性。
- 图片/字体:使用 Cache First,减少重复下载。
- 用户头像/第三方资源:使用 Stale-While-Revalidate,在速度和更新间平衡。
- 不可变资源(带哈希文件):使用 Cache First 或仅靠预缓存即可。
配置示例(使用 Workbox 路由):
import {registerRoute} from 'workbox-routing';
import {NetworkFirst, CacheFirst, StaleWhileRevalidate} from 'workbox-strategies';
registerRoute(
/\/api\/.*/,
new NetworkFirst({ cacheName: 'api-cache' })
);
registerRoute(
/\.(?:png|jpg|jpeg|svg|gif)$/,
new CacheFirst({ cacheName: 'image-cache' })
);
更新 Service Worker 与缓存清理
SW 本身不随页面刷新更新,浏览器会持续检测脚本的字节变化。当你发布新版本时需注意:
- 要求页面刷新:默认情况下,新 SW 安装后会等待旧 SW 失效(用户关闭所有标签页)。
skipWaiting()结合clients.claim()可实现平滑更新,但需要配合页面刷新逻辑。 - 缓存版本化管理:更改缓存名称(如
v2),在activate事件中删除旧版。 - 提示用户更新:监听
controllerchange事件或在 SW 内通过postMessage通知页面,显示“有新版本,点击刷新”的提示。
// 页面监听更新
navigator.serviceWorker.addEventListener('controllerchange', () => {
window.location.reload();
});
实战:打造一个离线可用的应用
完整代码骨架:
sw.js
const CACHE_NAME = 'app-v1';
const PRECACHE_URLS = ['/','/main.css','/app.js'];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(PRECACHE_URLS))
.then(() => self.skipWaiting())
);
});
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys => Promise.all(
keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key))
)).then(() => self.clients.claim())
);
});
self.addEventListener('fetch', event => {
if (event.request.method !== 'GET') return;
event.respondWith(
caches.match(event.request).then(cached => {
const fetched = fetch(event.request).then(response => {
const respClone = response.clone();
caches.open(CACHE_NAME).then(cache => cache.put(event.request, respClone));
return response;
}).catch(() => cached);
return cached || fetched;
})
);
});
注册 Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW 注册成功'))
.catch(err => console.error('SW 注册失败', err));
});
}
此示例采用 Stale-While-Revalidate 风格混合:有缓存直接返回,同时后台更新;无缓存则网络请求并缓存。
调试与常见坑
- 使用 Chrome DevTools:Application → Service Workers 面板可查看状态、强制更新、模拟离线。
- 缓存存储查看:Application → Cache Storage 可浏览精确内容。
- 更改 SW 后不生效:确认文件无语法错误,勾选 DevTools 中的 “Update on reload”。
- 过期的缓存页面:通过版本化缓存名解决,切勿改变同一缓存名中资源的含义。
- 跨域资源:跨域资源需设置
mode: 'cors'才能成功缓存Response。 - 存储空间限制:浏览器对每个源有配额,尽早清理无用缓存。
- 敏感数据:不要在缓存中存储用户私有信息,或在注销时清除相关缓存。
Service Worker 缓存是构建现代 Web 应用的基石之一。理解其生命周期、巧妙组合缓存策略,你就能为用户提供几乎瞬时的加载体验和真正的离线支持。进阶推荐使用 Workbox 等工具降低维护成本,使缓存方案更健壮、更易扩展。