fetch 事件与请求拦截
约 713 字大约 2 分钟
PWAService Workerfetch
2026-04-08
fetch 事件
Service Worker 通过监听 fetch 事件来拦截其 scope 下的所有网络请求(包括页面导航、图片、脚本、XHR、fetch API 等)。
self.addEventListener('fetch', (event) => {
// event.request 是一个只读的 Request 对象
console.log(event.request.url, event.request.method);
event.respondWith(
// 必须返回一个 Response 或 Promise<Response>
fetch(event.request)
);
});FetchEvent 核心 API
| 属性/方法 | 说明 |
|---|---|
event.request | 只读 Request 对象 |
event.clientId | 发起请求的客户端 ID |
event.respondWith(res) | 劫持请求,用指定的 Response 响应,不调用则走浏览器默认流程 |
event.waitUntil(p) | 延长 SW 存活时间,直到 Promise resolve(常用于写缓存) |
⚠️
respondWith必须 同步调用(在事件处理器的当前 tick 内),否则拦截失效。
基础请求拦截
1. 网络优先,失败回退缓存
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request).catch(() => caches.match(event.request))
);
});2. 缓存优先,失败回退网络
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => cached || fetch(event.request))
);
});Request / Response 复用问题
Request 和 Response 对象的 body 是 一次性流,读取后即耗尽。若要同时给缓存和浏览器,必须 clone:
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request).then((response) => {
const copy = response.clone(); // 克隆后才能放入缓存
caches.open('runtime').then((cache) => cache.put(event.request, copy));
return response;
})
);
});过滤不需要拦截的请求
拦截所有请求成本高且容易出错,应按需过滤:
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// 1. 只处理 GET
if (request.method !== 'GET') return;
// 2. 跳过跨域(除非明确要缓存)
if (url.origin !== self.location.origin) return;
// 3. 跳过 chrome-extension 等非 HTTP
if (!url.protocol.startsWith('http')) return;
// 4. 跳过 Range 请求(视频流)
if (request.headers.has('range')) return;
event.respondWith(handleRequest(request));
});根据资源类型分流
async function handleRequest(request) {
const url = new URL(request.url);
// 页面导航 → Network First
if (request.mode === 'navigate') {
return networkFirst(request, 'pages');
}
// 静态资源 → Cache First
if (/\.(js|css|woff2?|png|jpg|svg)$/.test(url.pathname)) {
return cacheFirst(request, 'assets');
}
// API → Stale While Revalidate
if (url.pathname.startsWith('/api/')) {
return staleWhileRevalidate(request, 'api');
}
return fetch(request);
}离线兜底页面
当用户离线访问未缓存的页面时,返回一个友好的 offline 页面:
const OFFLINE_URL = '/offline.html';
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('offline').then((cache) => cache.add(OFFLINE_URL))
);
});
self.addEventListener('fetch', (event) => {
if (event.request.mode !== 'navigate') return;
event.respondWith(
fetch(event.request).catch(() => caches.match(OFFLINE_URL))
);
});修改请求或响应
修改请求头
const modified = new Request(event.request, {
headers: { ...Object.fromEntries(event.request.headers), 'X-From-SW': '1' }
});
event.respondWith(fetch(modified));改写响应内容
event.respondWith(
fetch(event.request).then(async (res) => {
if (!res.headers.get('content-type')?.includes('text/html')) return res;
const text = await res.text();
const replaced = text.replace('{{USER}}', 'ZhenYu');
return new Response(replaced, {
status: res.status,
headers: res.headers
});
})
);常见坑
respondWith必须同步调用,await后再调用会失效- 响应对象只能用一次,要给多处使用必须
clone() - POST 请求默认不能放入 Cache API(只支持 GET),需要用 IndexedDB 自行实现
- 重定向请求:如果想缓存重定向后的最终响应,需要设置
redirect: 'follow'并小心处理 - opaque 响应(跨域
no-cors)大小显示为 0,会占用较大配额
