Service Worker
Service Worker 是一种运行在浏览器后台的 Worker,可以作为网络代理拦截和处理页面请求,实现离线缓存、后台同步和推送通知等功能。它是 Progressive Web App (PWA) 的核心技术。本节介绍 Service Worker 的生命周期、Cache API 以及基本的离线缓存实现。
前置知识
阅读本节前,建议先了解:SharedWorker
基础概念
Service Worker 的定位
Service Worker 本质上是一个网络代理,位于 Web 应用和服务器之间,可以拦截和控制所有的网络请求。与普通 Worker 不同,Service Worker 具有以下特点:
| 特性 | 说明 |
|---|---|
| 网络代理 | 拦截页面发出的所有请求 |
| 离线支持 | 可以缓存资源,实现离线访问 |
| 生命周期 | 独立于页面,可在后台运行 |
| 安全要求 | 必须在 HTTPS 下使用 |
| 作用域 | 控制在其注册路径下的所有页面 |
Service Worker 生命周期
注册 Service Worker
│
├── 1. 安装(install)
│ └── 预缓存静态资源
│
├── 2. 激活(activate)
│ └── 清理旧缓存
│
├── 3. 控制(fetch)
│ └── 拦截网络请求
│
└── 4. 更新(update)
└── 重新安装和激活语法与注册
注册 Service Worker
javascript
// 主线程 - 注册 Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js', {
scope: '/' // 控制范围(默认为 sw.js 所在目录)
})
.then(registration => {
console.log('Service Worker 注册成功:', registration.scope);
})
.catch(error => {
console.error('注册失败:', error);
});
// 监听更新
navigator.serviceWorker.addEventListener('updatefound', () => {
const newWorker = registration.installing;
console.log('发现新版本的 Service Worker');
});
}Service Worker 脚本 (sw.js)
javascript
// sw.js - Service Worker 脚本
// ========== 1. 安装事件 ==========
self.addEventListener('install', (event) => {
console.log('Service Worker 安装中...');
// 预缓存核心资源
event.waitUntil(
caches.open('app-cache-v1')
.then(cache => {
return cache.addAll([
'/',
'/index.html',
'/styles.css',
'/app.js',
'/logo.png'
]);
})
.then(() => self.skipWaiting()) // 立即激活
);
});
// ========== 2. 激活事件 ==========
self.addEventListener('activate', (event) => {
console.log('Service Worker 激活中...');
// 清理旧版本缓存
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== 'app-cache-v1')
.map(name => caches.delete(name))
);
}).then(() => self.clients.claim()) // 立即控制所有页面
);
});
// ========== 3. 请求拦截 ==========
self.addEventListener('fetch', (event) => {
event.respondWith(
// 优先从缓存读取,缓存未命中则从网络获取
caches.match(event.request)
.then(cachedResponse => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(event.request).then(response => {
// 缓存新获取的资源
if (response.status === 200) {
const responseClone = response.clone();
caches.open('app-cache-v1').then(cache => {
cache.put(event.request, responseClone);
});
}
return response;
});
})
);
});详细说明
生命周期详解
install 事件
javascript
self.addEventListener('install', (event) => {
// event.waitUntil() 确保 promise 完成后再完成安装
event.waitUntil(
caches.open('my-cache-v1').then(cache => {
return cache.addAll([
'/index.html',
'/styles.css',
'/app.js'
]);
})
);
// self.skipWaiting() 跳过等待,立即激活
// self.skipWaiting();
});activate 事件
javascript
self.addEventListener('activate', (event) => {
event.waitUntil(
// 获取所有缓存名称
caches.keys().then(keys => {
// 删除非当前版本的缓存
return Promise.all(
keys
.filter(key => key !== 'my-cache-v1')
.map(key => caches.delete(key))
);
})
);
// self.clients.claim() 立即控制所有页面
// self.clients.claim();
});fetch 事件
javascript
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// 只处理同源请求
if (url.origin === location.origin) {
event.respondWith(
caches.match(event.request).then(cached => {
return cached || fetch(event.request);
})
);
}
});Cache API 详解
javascript
// ========== 打开缓存 ==========
const cache = await caches.open('my-cache');
// ========== 添加资源 ==========
// 添加单个
await cache.add('/index.html');
// 添加多个
await cache.addAll(['/styles.css', '/app.js', '/logo.png']);
// 手动添加(需要 Response 对象)
await cache.put('/api/data', new Response(JSON.stringify({ id: 1 })));
// ========== 查询资源 ==========
// 精确匹配
const response = await cache.match('/index.html');
// 模糊匹配
const responses = await cache.matchAll(request => {
return request.url.includes('.css');
});
// ========== 删除资源 ==========
await cache.delete('/old-page.html');
// ========== 获取所有缓存 ==========
const keys = await caches.keys();
// ['my-cache-v1', 'my-cache-v2']
// ========== 删除缓存 ==========
await caches.delete('my-cache-v1');缓存策略
| 策略 | 说明 | 适用场景 |
|---|---|---|
| Cache First | 优先缓存,未命中再网络 | 静态资源 |
| Network First | 优先网络,失败再缓存 | API 请求 |
| Stale While Revalidate | 返回缓存同时更新 | 频繁更新的资源 |
| Cache Only | 仅使用缓存 | 离线页面 |
javascript
// 策略一:Cache First(缓存优先)
function cacheFirst(event) {
event.respondWith(
caches.match(event.request).then(cached => {
return cached || fetch(event.request);
})
);
}
// 策略二:Network First(网络优先)
function networkFirst(event) {
event.respondWith(
fetch(event.request)
.then(response => {
const clone = response.clone();
caches.open('dynamic-cache').then(cache => cache.put(event.request, clone));
return response;
})
.catch(() => caches.match(event.request))
);
}
// 策略三:Stale While Revalidate
function staleWhileRevalidate(event) {
event.respondWith(
caches.match(event.request).then(cached => {
const fetchPromise = fetch(event.request).then(response => {
caches.open('dynamic-cache').then(cache => cache.put(event.request, response.clone()));
return response;
});
return cached || fetchPromise;
})
);
}
// 在 fetch 事件中使用
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (url.pathname.match(/\.(js|css|png|jpg|svg)$/)) {
cacheFirst(event); // 静态资源用缓存优先
} else if (url.pathname.startsWith('/api/')) {
networkFirst(event); // API 用网络优先
}
});实战示例
基础离线缓存 Service Worker
javascript
// sw.js - 完整的 Service Worker 示例
const CACHE_NAME = 'my-app-v1';
const STATIC_ASSETS = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/favicon.ico'
];
// 安装:预缓存静态资源
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(STATIC_ASSETS))
.then(() => self.skipWaiting())
);
});
// 激活:清理旧缓存
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(keys => {
return Promise.all(
keys
.filter(key => key !== CACHE_NAME)
.map(key => caches.delete(key))
);
}).then(() => self.clients.claim())
);
});
// 拦截请求:缓存优先 + 网络回退
self.addEventListener('fetch', (event) => {
// 跳过非 GET 请求
if (event.request.method !== 'GET') return;
// 跳过跨域请求
if (!event.request.url.startsWith(self.location.origin)) return;
event.respondWith(
caches.match(event.request).then(cached => {
if (cached) {
// 返回缓存,后台更新
fetch(event.request).then(response => {
if (response.ok) {
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, response);
});
}
}).catch(() => {}); // 忽略网络错误
return cached;
}
// 无缓存,从网络获取
return fetch(event.request).then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const clone = response.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, clone);
});
return response;
});
}).catch(() => {
// 网络和缓存都失败,返回离线页面
if (event.request.mode === 'navigate') {
return caches.match('/index.html');
}
return new Response('离线不可用', { status: 503 });
})
);
});javascript
// 主页面注册代码
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(reg => {
console.log('SW 注册成功');
// 检测更新
reg.addEventListener('updatefound', () => {
const newWorker = reg.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'activated') {
console.log('新版本已激活,刷新页面');
// 可提示用户刷新
}
});
});
})
.catch(err => console.error('SW 注册失败:', err));
});
}注意事项
- 必须使用 HTTPS:Service Worker 只能在安全上下文中注册(localhost 除外)
- 作用域限制:Service Worker 只能控制其注册路径下的页面
- 首次加载无缓存:第一次访问页面时 Service Worker 还未安装
- 需要用户交互才能更新:新版本需要所有标签页关闭后才能激活
- 调试工具:Chrome DevTools > Application > Service Workers
最佳实践
- 使用版本号管理缓存(
cache-v1、cache-v2) - 在
install中预缓存核心资源,fetch中动态缓存其他资源 - 在
activate中清理旧版本缓存 - 为不同类型资源使用不同缓存策略
- 提供离线降级页面,提升用户体验
下一节
继续学习:WebSocket 基础