第4章 Fetch事件拦截:请求拦截与响应替换、fallback页面、离线资源兜底、请求日志
Service Worker 的 fetch 事件是离线优先能力的核心入口。通过拦截页面的所有网络请求(HTML、CSS、JS、图片、API 等),开发者可以决定使用缓存、网络或同时使用两者,从而精确控制每个资源的交付方式。本章深入讲解 fetch 事件的拦截机制、响应替换、fallback 页面设计、离线资源兜底策略以及请求日志记录,帮助你构建一个可观测、可恢复的离线应用。
4.1 原理说明
浏览器每次发起网络请求时,如果当前页面注册了 Service Worker,都会触发 Service Worker 的 fetch 事件。事件对象 FetchEvent 包含 request 属性和一个 respondWith() 方法。开发者通过 respondWith() 返回一个 Response 对象(来自缓存或网络),从而完全接管该请求的响应。拦截范围覆盖同源请求,也可通过 no-cors 模式处理跨域资源(但响应体不可读)。
本章的核心策略包括:
- 缓存优先 (Cache First) – 先查缓存,命中则返回,否则发网络请求并缓存。
- 网络优先 (Network First) – 先请求网络,失败或超时则降级到缓存。
- 仅网络 / 仅缓存 – 适用于特定资源。
- fallback 页面 – 当请求失败且缓存无匹配时,返回一个预定义的离线页面。
- 请求日志 – 将每次请求的 url、时间戳、状态、处理方式等记录到 IndexedDB。
4.2 实现流程
- 注册 Service Worker – 确保页面与 SW 建立联系,SW 进入激活状态。
- 在 activate 事件中清理旧缓存 – 避免缓存膨胀。
- 监听 fetch 事件 – 根据 url、请求类型等条件分发处理。
- 执行缓存策略 – 调用 Cache API 进行匹配、打开、存入操作。
- 网络降级与 fallback – 若网络返回错误或超时,尝试缓存或返回 fallback 页面。
- 记录请求日志 – 将结果写入 IndexedDB(异步,不阻塞响应返回)。
4.3 代码示例
以下为一个完整的 sw.js 核心片段,展示了 fetch 事件拦截、缓存优先、网络降级、fallback 页面以及请求日志记录。
// sw.js – Fetch 事件拦截与响应替换
const CACHE_NAME = 'v1';
const FALLBACK_URL = '/offline.html';
self.addEventListener('install', (event) => {
// 预缓存 fallback 页面和核心资源
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll([
'/offline.html',
'/style.css',
'/app.js'
]);
})
);
});
self.addEventListener('activate', (event) => {
// 清理旧版本缓存
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then((keyList) => {
return Promise.all(keyList.map((key) => {
if (!cacheWhitelist.includes(key)) {
return caches.delete(key);
}
}));
})
);
});
self.addEventListener('fetch', (event) => {
const requestUrl = new URL(event.request.url);
// 对同源请求执行缓存优先策略
if (requestUrl.origin === location.origin) {
event.respondWith(
cacheFirstWithFallback(event.request)
);
} else {
// 跨域资源:仅网络,不缓存
event.respondWith(fetch(event.request));
}
// 异步记录请求日志(不阻塞响应)
event.waitUntil(
logRequest(event.request, /* 占位,实际结果在respondWith之后 */)
);
});
async function cacheFirstWithFallback(request) {
const cache = await caches.open(CACHE_NAME);
let cachedResponse = await cache.match(request);
if (cachedResponse) {
// 缓存命中,返回缓存并异步更新缓存(stale-while-revalidate)
updateCache(request);
return cachedResponse;
}
// 缓存未命中,尝试网络
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
// 只缓存成功的响应(避免缓存错误页面)
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (err) {
// 网络失败,返回 fallback 页面(仅限文档请求)
if (request.destination === 'document') {
const fallback = await cache.match(FALLBACK_URL);
if (fallback) return fallback;
}
// 其他资源可返回空响应或占位图
return new Response('Offline', { status: 503, statusText: 'Service Unavailable' });
}
}
async function updateCache(request) {
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, networkResponse);
}
} catch (e) {
// 静默失败,不影响当前页面
}
}
// 请求日志写入 IndexedDB
async function logRequest(request, result) {
const db = await openDB();
const tx = db.transaction('log', 'readwrite');
const store = tx.objectStore('log');
store.add({
url: request.url,
method: request.method,
timestamp: Date.now(),
result: result // 可改为实际状态
});
await tx.done;
}
function openDB() {
return new Promise((resolve, reject) => {
const req = indexedDB.open('RequestLogDB', 1);
req.onupgradeneeded = (e) => {
const db = e.target.result;
if (!db.objectStoreNames.contains('log')) {
db.createObjectStore('log', { autoIncrement: true });
}
};
req.onsuccess = (e) => resolve(e.target.result);
req.onerror = (e) => reject(e.target.error);
});
}
event.waitUntil() 包装异步日志操作,可以确保 Service Worker 在写入完成前不会被终止。日志写入不应阻塞 respondWith(),因此先调用 respondWith(),再将日志任务放入 waitUntil 中。
4.4 调试方法
1. 在 Chrome DevTools 中打开 Application 面板,选择 Service Workers,可以查看当前注册的 SW 并触发 “Update” 和 “Unregister”。
2. 在 Cache Storage 中检查预缓存和动态缓存的内容。
3. 在 Network 面板中,勾选 “Disable cache” 可以观察离线行为;勾选 “Offline” 模拟无网络场景。
4. 打开 Console,查看 console.log 输出或错误提示。可以在 SW 中添加 self.addEventListener('message', ...) 实现更复杂的调试日志。
4.5 部署步骤
- 将
sw.js和offline.html放置于站点的根目录(或任意可访问路径),确保作用域覆盖需要离线使用的页面。 - 在主页面(如
index.html)中注册 SW:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js', { scope: '/' })
.then(() => console.log('SW registered'))
.catch(err => console.error('SW registration failed:', err));
}
- 使用 HTTPS(或 localhost)测试,因为 Service Worker 只在安全上下文中生效。
- 通过 DevTools 模拟离线场景,验证 fallback 页面和缓存策略是否正确。
4.6 常见错误处理
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| SW 注册后不触发 fetch 事件 | SW 作用域不包含当前页面,或页面被强制刷新跳过 SW | 检查 scope 参数;关闭 DevTools 的 "Bypass for network" 选项 |
| 跨域请求被拦截后无法读取响应 | 跨域资源使用 no-cors 模式,响应体为 opaque | 只对跨域资源进行网络请求,不尝试缓存 opaque 响应;或配置 CORS 头 |
| Fallback 页面显示空白 | 预缓存时未包含 fallback 页面,或路由不匹配 | 在 install 事件中添加 cache.addAll(['/offline.html']);确保 FALLBACK_URL 路径正确 |
| 日志未写入 IndexedDB | IndexedDB 版本更新失败,或事务未提交 | 检查 onupgradeneeded 中的 store 创建;使用 await tx.done |
| 缓存一直使用旧版资源 | 缓存未清除旧版本 | 在 activate 事件中清理旧缓存;使用版本号如 CACHE_NAME = 'v2' |
4.7 性能与稳定性优化建议
- 避免过度缓存 – 只缓存必要的资源(HTML、CSS、JS、字体、关键图片),排除 API 响应或用户数据。
- 使用 stale-while-revalidate – 对于频繁更新的资源,先返回缓存版本,再异步更新缓存,提升感知速度。
- 控制并发请求 – 在 fetch 事件中,对于同一 URL 的重复请求,可以使用
Request对象进行去重,避免同时写入缓存导致竞态。 - 合理设置超时 – 对于网络优先策略,可以设置超时(如 5 秒),超时后降级到缓存。
- 日志节流 – 日志写入频率过高可能影响性能,建议在写入前合并或使用批量写入。
// 去重示例:使用闭包缓存 pending 请求
const pendingRequests = new Map();
async function dedupedFetch(request) {
if (pendingRequests.has(request.url)) {
return pendingRequests.get(request.url);
}
const promise = fetch(request).finally(() => {
pendingRequests.delete(request.url);
});
pendingRequests.set(request.url, promise);
return promise;
}
respondWith() 路径轻量,将日志、缓存更新等操作放在 waitUntil() 中异步执行。
通过本章的学习,你已经掌握了 Fetch 事件拦截的核心机制,能够在 Service Worker 中灵活地实施缓存策略、提供离线 fallback 页面,并建立可观测的请求日志系统。这些技能是构建稳健离线优先应用的基石。