跳转到内容

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 处理器的最后一个分支,而不是一种独立策略。检查顺序决定了用户看到什么:

  1. fetch(event.request) 被拒绝。它只在网络层失败(DNS、TCP、TLS 或连接被中止)时拒绝;404 或 503 会正常兑现,所以服务器错误不是离线状况,应原样返回。
  2. caches.match(event.request) 返回 undefined,即这个精确 URL 从未被缓存或已被驱逐。
  3. 处理器按请求类型返回预缓存的兜底:event.request.mode === 'navigate' 返回 /offline.html,event.request.destination === 'image' 返回占位 SVG,/api/ 路径返回合成的 JSON 正文。
  4. 其他情况返回 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 写法,以及页面如何判断兜底是否已就位。

在 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 事件驱动显示一条朴素的“连接已断开”消息。

sw.js
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.js
if (navigator.serviceWorker?.controller) {
// 受控页面:导航失败时会得到 /offline.html。
} else {
window.addEventListener('offline', () => showBanner('连接已断开。恢复在线后会保存你的更改。'));
}

Workbox 补上了手写版本省略的响应克隆、状态过滤与缓存命名,代价是需要一个生成 __WB_MANIFEST 的构建步骤;对顶层导航而言 destination === 'document' 与上面的 mode === 'navigate' 守卫等价,并且还会匹配 iframe。

规范

规范状态
无。