跳转到内容

Service worker 离线兜底

一句话: 离线兜底是一个预缓存的 HTML 页面(或 API 响应),当导航请求失败且所请求的 URL 没有缓存版本时,service worker 会返回它——让用户看到有用的信息,而非浏览器错误页面。

当 Network First 策略失败(无网络且该 URL 无缓存)时,service worker 有两种选择:

  1. 放行请求——浏览器显示自己的离线错误页面(对用户来说是死路一条)。
  2. 返回预缓存的兜底页面——一个品牌化、有帮助的响应,告知用户当前处于离线状态。

兜底页面不能替代逐 URL 的缓存;它是从未加载过的 URL 或缓存条目已过期时的安全网。

install 事件期间预缓存离线页面,确保它始终可用:

const FALLBACK_URL = '/offline.html';
const CACHE_NAME = 'offline-fallback-v1';
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.add(FALLBACK_URL))
);
});

对于单页应用,兜底页面通常是 /index.html(应用外壳),它已被预缓存。如果外壳本身能渲染出有用的“您当前离线”状态,就无需单独的离线页面。

仅对网络和缓存均失败的导航请求返回兜底页面:

self.addEventListener('fetch', (event) => {
if (event.request.mode !== 'navigate') return;
event.respondWith(
fetch(event.request).catch(() =>
caches.match(event.request).then(
(cached) =>
cached ?? caches.open(CACHE_NAME).then((c) => c.match(FALLBACK_URL))
)
)
);
});

这个模式的流程:

  1. 尝试网络请求。
  2. 失败时,检查该 URL 是否有缓存版本。
  3. 仍无结果时,返回通用离线兜底页面。

对于非导航请求,可以返回兜底 SVG 或占位符,而非网络错误:

const IMAGE_FALLBACK = '/fallback-image.svg';
self.addEventListener('fetch', (event) => {
if (event.request.destination === 'image') {
event.respondWith(
fetch(event.request).catch(() => caches.match(IMAGE_FALLBACK))
);
}
});

对于 JSON API 请求,返回结构化的兜底内容而非 HTML 错误页面:

self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (url.pathname.startsWith('/api/')) {
event.respondWith(
fetch(event.request).catch(() =>
new Response(JSON.stringify({ error: 'offline' }), {
headers: { 'Content-Type': 'application/json' },
status: 503,
})
)
);
}
});

Workbox 的 setCatchHandler 是处理未匹配 fetch 失败的惯用方式:

import { setCatchHandler, NavigationRoute, registerRoute } from 'workbox-routing';
import { precacheAndRoute, matchPrecache } from 'workbox-precaching';
// 将兜底页面作为清单的一部分预缓存
precacheAndRoute(self.__WB_MANIFEST); // 包含 /offline.html
// 对任何失败的导航请求返回离线页面
setCatchHandler(async ({ event }) => {
if (event.request.destination === 'document') {
return matchPrecache('/offline.html');
}
return Response.error();
});

一个好的离线兜底页面应当:

  • 清晰告知用户处于离线状态——不要让用户猜测为何内容加载失败。
  • 无需网络即可正常运行——避免需要 API 调用的脚本。页面必须仅凭其预缓存的 HTML 即可渲染。
  • 体现产品的视觉风格——不要使用通用的浏览器错误样式。
  • 在条件允许时链接到已缓存的内容(例如“您最近访问过的页面”)。
  • install 中用 event.waitUntil(cache.add('/offline.html')) 预缓存兜底页面。
  • 通用兜底仅用于导航请求(mode === 'navigate');子资源失败应静默降级或返回类型化兜底内容。
  • 通过 DevTools → 网络 → “离线”并导航到未缓存的 URL 来测试兜底效果。
  • 对于 SPA,确认应用外壳本身能优雅地处理离线状态,无需单独的离线页面。
  • 为兜底缓存名称加版本号,并在 activate 中与其他旧缓存一并清理。
  • 缓存策略 — Network First 是最常与兜底搭配使用的策略
  • Cache API — 预缓存用 cache.add();请求时用 caches.match()
  • WorkboxsetCatchHandlermatchPrecache