# Service worker 离线兜底

> 请求失败且 URL 无缓存时，service worker 如何返回预缓存的页面、图片或 JSON，Response.error() 做什么，以及 Workbox 的等价写法。

离线兜底是 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 字体：每一个都会以同样的网络错误失败，页面将无样式渲染。

:::observed
导航失败且没有 service worker 兜底时，Chrome（英文界面）显示自己的插页，标题为 `No internet`，错误码为 `ERR_INTERNET_DISCONNECTED`，DevTools Console 记录 `GET https://example.com/ net::ERR_INTERNET_DISCONNECTED`；一旦 `fetch` 处理器返回了预缓存的 `/offline.html`，插页不再出现，该导航也不再记录 `net::ERR_*` 行，但 worker 自己失败的 `fetch()` 仍会记录一行。`ERR_INTERNET_DISCONNECTED` 是 Chromium 的网络错误名，显示在插页上，也列在 `chrome://network-errors/` 中。
:::

## 示例

第一个示例为导航与图片给出显式兜底，其余请求返回 `Response.error()`；第二个示例给出 Workbox 写法，以及页面如何判断兜底是否已就位。

### 预缓存并提供页面与图片兜底

在 `install` 中把两个资源一起预缓存，让 worker 在缺少它们时拒绝安装。`fetch` 处理器只介入有兜底的两种请求类型；其他请求原样放行到网络。

```js
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 与页面侧检测

`setCatchHandler` 在所有已注册路由的处理器都抛错时运行，失败的 `NetworkFirst` 正是如此。`matchPrecache()` 从 `precacheAndRoute()` 建立的预缓存中读取，因此 `/offline.html` 必须在构建清单里。页面侧，对兜底 URL 发一次 `HEAD` 请求（worker 激活时从缓存应答）可以知道兜底是否已安装；没有 worker 时，页面改为由 `offline` 事件驱动显示一条朴素的"连接已断开"消息。

```js
// 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。

## 另请参阅

- [Manage fallback responses](https://developer.chrome.com/docs/workbox/managing-fallback-responses/)（developer.chrome.com）
- [Response: error() static method](https://developer.mozilla.org/en-US/docs/Web/API/Response/error_static)（developer.mozilla.org）
- [Service worker 缓存策略](/zh/reference/service-worker/caching-strategies/)
- [Cache API](/zh/reference/service-worker/cache-api/)
- [FetchEvent 与请求路由](/zh/reference/service-worker/fetch-event/)
- [Workbox](/zh/reference/service-worker/workbox/)