Service worker fetch 事件与路由
一句话: 页面在 service worker scope 范围内发起的每一个网络请求,都会在 worker 中触发一个 fetch 事件。event.respondWith(promise) 让 worker 拦截请求并返回任意 Response——来自缓存、网络或内联构造。
FetchEvent 接口
Section titled “FetchEvent 接口”FetchEvent 包含以下内容:
event.request—Request对象,包含url、method、headers、destination和mode。event.respondWith(responsePromise)— 调用此方法以接管响应。若不调用,请求会像往常一样直接走网络。event.waitUntil(promise)— 在事件结束后延长 worker 的生命周期,用于缓存预热等副作用操作。event.clientId— 发出请求的客户端(标签页)标识符。event.resultingClientId— 若导航请求创建了新客户端,为新客户端的 ID。
self.addEventListener('fetch', (event) => { // 只拦截同源请求 if (!event.request.url.startsWith(self.location.origin)) return;
event.respondWith( caches.match(event.request).then((cached) => cached ?? fetch(event.request)) );});不调用 respondWith() 直接返回,请求会穿透到浏览器正常的网络栈。
用于路由决策的请求属性
Section titled “用于路由决策的请求属性”| 属性 | 取值与含义 |
|---|---|
request.destination |
'document'、'script'、'image'、'font'、'fetch'、''(XHR/fetch 无类型) |
request.mode |
'navigate'(页面导航)、'cors'、'no-cors'、'same-origin' |
request.method |
'GET'、'POST'、'PUT' 等 |
request.url |
完整 URL 字符串;使用 new URL(event.request.url) 进行路径匹配 |
按 destination 路由
Section titled “按 destination 路由”self.addEventListener('fetch', (event) => { const { destination } = event.request;
if (destination === 'image') { event.respondWith(cacheFirst(event.request, 'images')); } else if (destination === 'document') { event.respondWith(networkFirst(event.request, 'pages')); } // 其他请求透传});按 URL 前缀路由
Section titled “按 URL 前缀路由”self.addEventListener('fetch', (event) => { const url = new URL(event.request.url);
if (url.pathname.startsWith('/api/')) { event.respondWith(networkOnly(event.request)); } else if (url.pathname.startsWith('/static/')) { event.respondWith(cacheFirst(event.request, 'static')); }});导航请求(单页应用离线支持)
Section titled “导航请求(单页应用离线支持)”对于单页应用,所有 navigate 请求应返回已缓存的应用外壳:
self.addEventListener('fetch', (event) => { if (event.request.mode === 'navigate') { event.respondWith( caches.match('/index.html').then((cached) => cached ?? fetch(event.request)) ); return; } // 处理其他请求类型…});内联构造 Response
Section titled “内联构造 Response”respondWith() 接受任意 Response,包括你自己构造的:
event.respondWith( new Response('<h1>离线中</h1>', { headers: { 'Content-Type': 'text/html' }, }));这是离线兜底页面的基础。详见离线兜底。
respondWith 无法覆盖的情况
Section titled “respondWith 无法覆盖的情况”- 不可拦截的请求。 来自
<link rel="preload">或第三方 iframe 的mode: 'no-cors'请求,根据上下文可能不触发fetch事件。 - 不透明响应。 以
no-cors方式获取的跨域请求会产生不透明响应(status: 0)。可以缓存并返回它们,但无法读取其 body 或状态码。 - POST 及写操作请求。
fetch事件对 POST 也会触发,但缓存写操作响应需要谨慎设计(离线推迟写操作请参阅后台同步)。
使用 Workbox 进行路由
Section titled “使用 Workbox 进行路由”Workbox 的 registerRoute 比在 fetch 监听器中写长 if/else 链更清晰:
import { registerRoute } from 'workbox-routing';import { CacheFirst, NetworkFirst } from 'workbox-strategies';
registerRoute(({ request }) => request.destination === 'image', new CacheFirst());registerRoute(({ request }) => request.mode === 'navigate', new NetworkFirst());完整详情请参阅 Workbox。
- 只有在确实要处理请求时才调用
event.respondWith();其他请求放行透传。 - 在缓存前检查
event.request.method——除非有明确需要,否则不要缓存非 GET 响应。 - 使用
new URL(event.request.url).origin区分同源与跨域请求,再进行路由。 - 处理缓存与网络均失败的情况——返回兜底
Response,避免空白错误页面。 - 将 DevTools 网络节流设置为“离线”来测试路由逻辑。