跳转到内容

Service worker fetch 事件与路由

一句话: 页面在 service worker scope 范围内发起的每一个网络请求,都会在 worker 中触发一个 fetch 事件。event.respondWith(promise) 让 worker 拦截请求并返回任意 Response——来自缓存、网络或内联构造。

FetchEvent 包含以下内容:

  • event.requestRequest 对象,包含 urlmethodheadersdestinationmode
  • 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() 直接返回,请求会穿透到浏览器正常的网络栈。

属性 取值与含义
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) 进行路径匹配
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'));
}
// 其他请求透传
});
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;
}
// 处理其他请求类型…
});

respondWith() 接受任意 Response,包括你自己构造的:

event.respondWith(
new Response('<h1>离线中</h1>', {
headers: { 'Content-Type': 'text/html' },
})
);

这是离线兜底页面的基础。详见离线兜底

  • 不可拦截的请求。 来自 <link rel="preload"> 或第三方 iframe 的 mode: 'no-cors' 请求,根据上下文可能不触发 fetch 事件。
  • 不透明响应。no-cors 方式获取的跨域请求会产生不透明响应(status: 0)。可以缓存并返回它们,但无法读取其 body 或状态码。
  • POST 及写操作请求。 fetch 事件对 POST 也会触发,但缓存写操作响应需要谨慎设计(离线推迟写操作请参阅后台同步)。

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 网络节流设置为“离线”来测试路由逻辑。
  • 缓存策略respondWith() 所调用的各种策略
  • Cache API — 处理器内部使用的 caches.match() 等方法
  • 离线兜底 — 当一切失败时构造有意义的响应
  • Workbox — 生产可用的路由与策略库