Service Worker · 概念
Service worker 离线兜底
发布于
离线兜底是 service worker 在 install 阶段存好、并在网络请求失败且 Cache Storage 中没有该 URL 时由 fetch 处理器返回的一份响应,让用户看到应用自己的页面而不是浏览器的错误页。它只用到 FetchEvent、cache.add() 与 caches.match(),三者都属 Baseline 广泛可用(Chrome 40、Firefox 44、Safari 11.1、Edge 17;BCD api.FetchEvent),所以这一模式在任何能运行 service worker 的地方行为一致。
兜底是 Network First 或 Cache First 处理器的最后一个分支,而不是一种独立策略。检查顺序决定了用户看到什么:
fetch(event.request)被拒绝。它只在网络层失败(DNS、TCP、TLS 或连接被中止)时拒绝;404或503会正常兑现,所以服务器错误不是离线状况,应原样返回。caches.match(event.request)返回undefined,即这个精确 URL 从未被缓存或已被驱逐。- 处理器按请求类型返回预缓存的兜底:
event.request.mode === 'navigate'返回/offline.html,event.request.destination === 'image'返回占位 SVG,/api/路径返回合成的 JSON 正文。 - 其他情况返回
Response.error(),这是一个type为'error'、status为0的网络错误响应,会让页面的fetch()像没有 worker 时一样被拒绝。
mode === 'navigate' 这道守卫很重要,因为浏览器自带的离线页只在导航时才是问题;给失败的脚本或图片请求返回 HTML 会被按错误的类型解析。兜底必须在 install 中用 cache.add()(或放进 addAll())缓存,这样兜底 URL 不可用时安装就会失败;惰性抓取的兜底恰恰会在需要它的时候缺席。兜底页上指向任何未缓存 URL 的 <a href> 会以同样方式失败,所以页面应只链接到已预缓存的路由,或自带一个调用 location.reload() 的重试按钮。
兜底 HTML 的状态码就是缓存响应原有的状态(预缓存文件为 200),除非处理器另行构造带 status: 503 的 Response;两者浏览器都会渲染,区别主要影响读取状态码的服务端分析。兜底页本身不应引用未缓存的脚本、样式表或 Web 字体:每一个都会以同样的网络错误失败,页面将无样式渲染。
第一个示例为导航与图片给出显式兜底,其余请求返回 Response.error();第二个示例给出 Workbox 写法,以及页面如何判断兜底是否已就位。
预缓存并提供页面与图片兜底
Section titled “预缓存并提供页面与图片兜底”在 install 中把两个资源一起预缓存,让 worker 在缺少它们时拒绝安装。fetch 处理器只介入有兜底的两种请求类型;其他请求原样放行到网络。
const FALLBACK_CACHE = 'fallback-v1';const OFFLINE_PAGE = '/offline.html';const OFFLINE_IMAGE = '/offline.svg';
self.addEventListener('install', (event) => { event.waitUntil( caches.open(FALLBACK_CACHE).then((cache) => cache.addAll([OFFLINE_PAGE, OFFLINE_IMAGE])) );});
self.addEventListener('fetch', (event) => { const { request } = event; if (request.mode !== 'navigate' && request.destination !== 'image') return;
event.respondWith((async () => { try { return await fetch(request); } catch { const cached = await caches.match(request); if (cached) return cached; const fallback = await caches.match(request.mode === 'navigate' ? OFFLINE_PAGE : OFFLINE_IMAGE); return fallback ?? Response.error(); } })());});给失败的 <img> 返回 SVG,除了占位图之外没有可见代价;给导航返回 /offline.html 会替换整个页面,所以处理器先查 caches.match(request),只在精确 URL 缺失时才兜底。最后的 Response.error() 为 worker 没有规划到的请求类型保留浏览器的原生行为。
Workbox 的 setCatchHandler 与页面侧检测
Section titled “Workbox 的 setCatchHandler 与页面侧检测”setCatchHandler 在所有已注册路由的处理器都抛错时运行,失败的 NetworkFirst 正是如此。matchPrecache() 从 precacheAndRoute() 建立的预缓存中读取,因此 /offline.html 必须在构建清单里。页面侧,对兜底 URL 发一次 HEAD 请求(worker 激活时从缓存应答)可以知道兜底是否已安装;没有 worker 时,页面改为由 offline 事件驱动显示一条朴素的“连接已断开”消息。
import { precacheAndRoute, matchPrecache } from 'workbox-precaching';import { setCatchHandler } from 'workbox-routing';
precacheAndRoute(self.__WB_MANIFEST); // 清单必须包含 /offline.html
setCatchHandler(async ({ request }) => { if (request.destination === 'document') return matchPrecache('/offline.html'); if (request.destination === 'image') return matchPrecache('/offline.svg'); return Response.error();});
// page.jsif (navigator.serviceWorker?.controller) { // 受控页面:导航失败时会得到 /offline.html。} else { window.addEventListener('offline', () => showBanner('连接已断开。恢复在线后会保存你的更改。'));}Workbox 补上了手写版本省略的响应克隆、状态过滤与缓存命名,代价是需要一个生成 __WB_MANIFEST 的构建步骤;对顶层导航而言 destination === 'document' 与上面的 mode === 'navigate' 守卫等价,并且还会匹配 iframe。
- Manage fallback responses(developer.chrome.com)
- Response: error() static method(developer.mozilla.org)
- Service worker 缓存策略
- Cache API
- FetchEvent 与请求路由
- Workbox
规范
| 规范 | 状态 |
|---|---|
| 无。 | |