Service worker 离线兜底
一句话: 离线兜底是一个预缓存的 HTML 页面(或 API 响应),当导航请求失败且所请求的 URL 没有缓存版本时,service worker 会返回它——让用户看到有用的信息,而非浏览器错误页面。
为什么需要兜底页面
Section titled “为什么需要兜底页面”当 Network First 策略失败(无网络且该 URL 无缓存)时,service worker 有两种选择:
- 放行请求——浏览器显示自己的离线错误页面(对用户来说是死路一条)。
- 返回预缓存的兜底页面——一个品牌化、有帮助的响应,告知用户当前处于离线状态。
兜底页面不能替代逐 URL 的缓存;它是从未加载过的 URL 或缓存条目已过期时的安全网。
预缓存兜底页面
Section titled “预缓存兜底页面”在 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(应用外壳),它已被预缓存。如果外壳本身能渲染出有用的“您当前离线”状态,就无需单独的离线页面。
提供兜底响应
Section titled “提供兜底响应”仅对网络和缓存均失败的导航请求返回兜底页面:
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)) ) ) );});这个模式的流程:
- 尝试网络请求。
- 失败时,检查该 URL 是否有缓存版本。
- 仍无结果时,返回通用离线兜底页面。
图片和字体的兜底
Section titled “图片和字体的兜底”对于非导航请求,可以返回兜底 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)) ); }});API 兜底响应
Section titled “API 兜底响应”对于 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
Section titled “使用 Workbox”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();});设计离线页面
Section titled “设计离线页面”一个好的离线兜底页面应当:
- 清晰告知用户处于离线状态——不要让用户猜测为何内容加载失败。
- 无需网络即可正常运行——避免需要 API 调用的脚本。页面必须仅凭其预缓存的 HTML 即可渲染。
- 体现产品的视觉风格——不要使用通用的浏览器错误样式。
- 在条件允许时链接到已缓存的内容(例如“您最近访问过的页面”)。
- 在
install中用event.waitUntil(cache.add('/offline.html'))预缓存兜底页面。 - 通用兜底仅用于导航请求(
mode === 'navigate');子资源失败应静默降级或返回类型化兜底内容。 - 通过 DevTools → 网络 → “离线”并导航到未缓存的 URL 来测试兜底效果。
- 对于 SPA,确认应用外壳本身能优雅地处理离线状态,无需单独的离线页面。
- 为兜底缓存名称加版本号,并在
activate中与其他旧缓存一并清理。