跳转到内容

Service Worker · API

FetchEvent 与请求路由

发布于

受控客户端发出的每一个导航请求与子资源请求,都会在 service worker 中派发一个 FetchEvent;调用 event.respondWith() 可以用任意 Response 取代网络响应,不调用则请求原样走网络。该接口自 Chrome 40、Edge 17、Firefox 44、Safari 11.1 起可用(BCD api.FetchEvent),后来加入的 preloadResponse 与 handled 在各自条目中说明。

self.addEventListener('fetch', (event) => {
event.respondWith(responsePromise); // 可选:接管响应
event.waitUntil(promise); // 可选:为副作用延长 worker 存活时间
});

respondWith() 必须在处理函数内同步调用;处理函数返回后,浏览器已经决定是否走网络。waitUntil() 则可以在事件仍在处理期间的任何时刻调用,包括在 respondWith() 的 Promise 链内部。

事件暴露请求对象与客户端标识;两个方法分别控制响应与 worker 的生命周期。

成员 类型 说明
request Request 被拦截的请求。url、method、headers、mode、destination、credentials 是路由时用到的属性。
respondWith(r) void 接受 Response 或 Promise<Response>。Promise 的落定结果就是客户端收到的响应。
waitUntil(p) void 把事件生命周期延长到 p 落定,这样在 respondWith() 之后发起的缓存写入不会因 worker 被终止而中断。
clientId string 发出请求的客户端 id。导航请求时为空,因为正在导航的客户端尚不存在。
resultingClientId string 导航将要创建的客户端 id。子资源请求时为空。
replacesClientId string 被导航取代的客户端 id(仅同窗口导航)。
handled Promise<void> 响应交付给客户端后兑现;事件以网络错误结束时拒绝(BCD api.FetchEvent.handled)。
preloadResponse Promise<Response | undefined> 导航预加载的响应;预加载未执行时为 undefined,见 NavigationPreloadManager 条目。

request.destination 是最可靠的路由键:导航为 'document',另有 'script'、'style'、'image'、'font',而 fetch() 与 XMLHttpRequest 调用为 ''。对顶层页面,request.mode === 'navigate' 与 destination === 'document' 等价,但前者也匹配 iframe,后者对 iframe 为 'iframe'。

respondWith() 同步抛错;响应侧的失败则以网络错误交给客户端,而不是 worker 内的异常。

  • InvalidStateError:在处理函数返回之后才调用 respondWith(),或对同一事件第二次调用。
  • 客户端收到网络错误:传给 respondWith() 的 Promise 被拒绝,或兑现为一个不是 Response 的值。Chromium 在 worker 控制台记录 The FetchEvent for "<url>" resulted in a network error response: an object that was not a Response was passed to respondWith().,页面侧则看到 TypeError: Failed to fetch(导航请求则是错误页)。
  • 客户端收到网络错误:Response 的 body 已被使用,例如未 clone() 就交给了 cache.put()。
  • new Request(event.request, init) 的 TypeError:请求是导航(mode: 'navigate')且 init 非空;导航请求需要改头时,改为从 event.request.url 构造新的 Request。

下面的处理函数都写成只有匹配的分支才调用 respondWith();其他请求全部放行给网络。

图片走缓存优先的辅助函数,API 调用绕过缓存,其余交给浏览器。

self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (url.origin !== self.location.origin) return; // 第三方:不拦截
if (event.request.destination === 'image') {
event.respondWith(cacheFirst(event.request, 'images-v1'));
} else if (url.pathname.startsWith('/api/')) {
event.respondWith(fetch(event.request)); // 显式的仅网络
}
});
async function cacheFirst(request, cacheName) {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
if (cached) return cached;
const response = await fetch(request);
if (response.ok) cache.put(request, response.clone());
return response;
}

即使是仅网络的路由,对其他源提前返回也很重要:一旦调用了 respondWith(),整个响应(包括 CORS 行为与 opaque 响应)就由 worker 负责。

单页应用对每次导航都用缓存的壳作答,并在后台刷新该壳,同时让 worker 为这次刷新保持存活。

self.addEventListener('fetch', (event) => {
if (event.request.mode !== 'navigate') return;
event.respondWith(
caches.match('/index.html').then((cached) => cached ?? fetch(event.request))
);
event.waitUntil(
fetch('/index.html').then((fresh) =>
fresh.ok ? caches.open('shell-v1').then((c) => c.put('/index.html', fresh)) : undefined
).catch(() => {})
);
});

没有 waitUntil() 时,浏览器可能在 respondWith() 一落定就终止 worker,丢掉后台刷新。catch 则避免离线刷新变成未处理的拒绝。

检测 service worker 支持,并在没有它时让请求照常工作

Section titled “检测 service worker 支持,并在没有它时让请求照常工作”

没有受控 worker 的页面(首次访问、非安全源、强制刷新)也必须能加载。按条件注册,并且不要让页面代码依赖拦截。

if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').catch((err) => {
console.warn('Service worker registration failed; running without offline support', err);
});
}
// 页面代码在两种情况下都以同样方式调用 fetch('/api/items'):
// worker 只是一层优化,没有 worker 接管时请求同样成立。

在 worker 控制页面之前 navigator.serviceWorker.controller 为 null,Chrome 与 Firefox 中强制刷新(Shift+Reload)之后也是这个状态;读取它的代码必须处理 null。

规范

规范状态
无。