Front-End-Checklist 之 offline-fallback:基于 Service Worker 实现自定义离线回退页的完整指南
2026/9/19 19:17:51 网站建设 项目流程

Front-End-Checklist 之 offline-fallback:基于 Service Worker 实现自定义离线回退页的完整指南

【免费下载链接】Front-End-Checklist🗂 The essential checklist for modern web development, for humans and AI agents项目地址: https://gitcode.com/gh_mirrors/fr/Front-End-Checklist

在弱网或完全断网的场景下,浏览器只会显示一张与你的产品毫无关系的默认错误页。本文基于 Front-End-Checklist 仓库中的offline-fallback技能文档(SKILL.md)及其完整规则说明(references/rule.md),系统讲解如何创建自包含的离线回退页、在 Service Worker 的install事件中预缓存、在fetch处理器中拦截失败导航请求并返回缓存页面,同时给出 Workbox 方案、在线/离线状态横幅以及可直接执行的验证清单。读完本文,你可以为任意站点落地一套可被 Lighthouse PWA 审计识别的离线体验。

规则定位:这条规则在 Checklist 中的坐标

offline-fallback是 Front-End-Checklist 规则库中performance/loading子分类下的一条规则,其元数据定义在 offline-fallback.mdx 的 frontmatter 中:

  • 分类performance,子分类loading
  • 优先级low
  • 难度beginner
  • 预估耗时:20 分钟
  • AI 适用场景aiContext):为站点添加 PWA 能力、实现 Service Worker、或改进弱网用户体验时使用

该文档同时面向 LLM Agent 提供了四个标准提示(prompts 字段),也是理解这条规则工程闭环的钥匙:

提示类型内容
check检查断网且导航请求无法被满足时,站点是否展示自定义离线回退页
fix创建自包含的/offline页面,并配置 Service Worker 预缓存它、在导航失败时返回它
explain解释 Service Worker 如何拦截失败的请求并返回缓存的离线页
codeReview审查 Service Worker 的 fetch 处理器与离线页标记,确认离线页在 install 时已预缓存、不依赖外部资源,且回退只作用于导航请求而非子资源

仓库中与本规则直接关联的是 service-worker.mdx:Service Worker 正是缓存并返回离线回退页的机制,两条规则被标注为“共同实现”。本文的后半部分将结合该规则的实现细节展开。

为什么需要自定义离线回退页

浏览器默认的离线错误屏(“No internet connection”)令人困惑,且完全脱离品牌。自定义离线页的价值在于:

  1. 维持用户体验连续性:用户停留在你的产品语境中,而不是被抛到一张系统级错误页;
  2. 强化信任:产品主动处理异常,传递出“可控”的信号;
  3. 承载可用动作:展示已缓存内容,或引导用户继续阅读一篇已缓存的文章、将一次表单提交排队等待网络恢复后再发送。

离线页必须满足的四条硬约束

  • 不发起任何网络请求—— 所有 CSS、JavaScript、图片必须内联或已被缓存;
  • 明确告知用户—— 清楚地说明“你已离线”;
  • 提供可操作选项—— 重试按钮、已缓存页面列表、或导航到已缓存内容的入口;
  • 贴合品牌—— 一致的 Logo、字体(预缓存字体或系统字体)与配色。

最后一点常被忽视:离线页引用的任何外部资源(字体、图片、脚本)本身都必须被预缓存或内联。如果离线页发起的请求失败,浏览器可能显示一片空白或破损页面——这比浏览器原生错误屏更糟。

第一步:创建自包含的 /offline 页面

把离线页做成一个零依赖的 HTML 文件,例如放在public/offline.html。以下是规则文档给出的完整参考实现(原文见 rule.md 的 Code Example 一节):

<!-- public/offline.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>You're offline — Acme App</title> <style> /* All styles must be inline — no external stylesheets */ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: system-ui, -apple-system, sans-serif; background: #f8fafc; color: #1e293b; display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 1.5rem; } .card { background: #fff; border: 1px solid #e2e8f0; border-radius: 1rem; padding: 2.5rem 2rem; max-width: 420px; width: 100%; text-align: center; } .icon { font-size: 3rem; margin-bottom: 1rem; } h1 { font-size: 1.5rem; font-weight: 700; margin-bottom: 0.5rem; } p { color: #64748b; line-height: 1.6; margin-bottom: 1.5rem; } button { background: #3b82f6; color: #fff; border: none; border-radius: 0.5rem; padding: 0.625rem 1.5rem; font-size: 0.95rem; cursor: pointer; transition: background 0.15s; } button:hover { background: #2563eb; } button:active { background: #1d4ed8; } </style> </head> <body> <div class="card"> <div class="icon" aria-hidden="true">📡</div> <h1>You're offline</h1> <p> It looks like you've lost your internet connection. Check your network settings and try again. </p> <button onclick="window.location.reload()">Try again</button> </div> </body> </html>

这份示例体现了上文四条硬约束的典型落法:样式全部写在<style>内联块中、字体使用system-ui系统字体栈避免字体文件请求、装饰图标用aria-hidden="true"对屏幕阅读器隐藏,以及一个直接调用window.location.reload()的重试按钮——重试逻辑不需要任何外部脚本。

第二步:在 install 事件中预缓存离线页

离线页只有被缓存后,离线时才能被caches.match()找到。因此要把/offline(或/offline.html)加入 Service Workerinstall事件的预缓存列表(原文见 rule.md 的 Step 2):

// public/sw.js const CACHE_NAME = 'static-v1' const PRECACHE_URLS = [ '/', '/offline', // ← the offline fallback page '/styles/main.css', '/scripts/app.js', ] self.addEventListener('install', (event) => { event.waitUntil( caches .open(CACHE_NAME) .then((cache) => cache.addAll(PRECACHE_URLS)) .then(() => self.skipWaiting()) ) })

几个实现要点:

  • event.waitUntil确保 Worker 安装流程会等待addAll完成;addAll中任何一个 URL 失败都会使整个安装失败,所以预缓存列表里的每一项都必须稳定可访问;
  • self.skipWaiting()让新版本 Worker 立即进入等待激活状态,避免用户必须关闭所有标签页才能拿到新缓存;
  • 缓存名static-v1即缓存版本。这一点在仓库的 service-worker 规则 中有专门展开:部署破坏性变更时应提升CACHE_VERSION,否则用户可能收到新旧文件混用的结果,并且给出用构建时间戳自动生成版本的示例(process.env.BUILD_ID ?? Date.now().toString())。

第三步:导航失败时返回离线页

fetch处理器中捕获导航请求的网络错误,并返回缓存中的离线页(原文见 rule.md 的 Step 3):

// public/sw.js (fetch handler) self.addEventListener('fetch', (event) => { // Only handle same-origin GET requests if ( event.request.method !== 'GET' || !event.request.url.startsWith(self.location.origin) ) { return } if (event.request.mode === 'navigate') { event.respondWith(handleNavigationRequest(event.request)) } }) async function handleNavigationRequest(request) { try { // Always try the network first for navigation const networkResponse = await fetch(request) // Cache a copy for later const cache = await caches.open(CACHE_NAME) cache.put(request, networkResponse.clone()) return networkResponse } catch { // Network failed — serve cached page if available, otherwise offline page const cached = await caches.match(request) if (cached) return cached const offlinePage = await caches.match('/offline') return ( offlinePage ?? new Response('<h1>Offline</h1>', { headers: { 'Content-Type': 'text/html' }, }) ) } }

这段逻辑的决策顺序值得逐层拆解:

  1. 只处理同源 GET:非 GET 请求(如 POST 提交)与跨域请求直接放行,避免把离线逻辑套到不该套的请求上;
  2. 只拦截导航请求event.request.mode === 'navigate'是导航的判据。这也正是codeReview提示的要求——回退只服务于导航请求,而不是图片、字体等子资源;
  3. 导航采用网络优先:导航请求总是先尝试网络,成功后顺手把响应clone()一份写入缓存,供下次离线使用(注意clone()的必要性:Response的正文流只能消费一次);
  4. 三级降级:网络失败后,先找该 URL 的历史缓存(用户之前访问过的页面),找不到再返回/offline页;
  5. 终极兜底:如果连/offline都不在缓存中(比如 Worker 安装失败),仍返回一个最简的<h1>Offline</h1>文本响应,保证用户永远不会看到白屏。

与 service-worker 规则的完整实现对照

仓库中 service-worker 规则 给出了同一机制的完整版实现,可以和本节的三个片段拼合成一个生产级 Service Worker:

  • 注册(L102-L137):在主入口尽早调用navigator.serviceWorker.register('/sw.js', { scope: '/' }),先用'serviceWorker' in navigator做能力探测,并在updatefound事件中提示用户有新版本待刷新;注册放在页面load之后,避免与关键资源竞争带宽;
  • install / activate 生命周期(L157-L183):activate事件会删除不在白名单内的旧缓存(即上一版本命名的缓存)并调用clients.claim(),这是离线页“更新即生效”的前提;
  • 按资源类型分策略(L186-L209):静态资源走cacheFirst,导航走networkFirstWithOfflineFallback,动态 API 走staleWhileRevalidate,其中导航策略的实现(L231-L248)与本文第三步的handleNavigationRequest是同一套“网络 → 该页缓存 →/offline”的三级降级;
  • 安全边界(L343-L345):不要缓存 POST 请求或带鉴权的 API 响应,缓存范围应限定在幂等的、非敏感的 GET 请求内。

增强:页面上的在线/离线状态横幅

离线回退页只覆盖“断网后发起新导航”的场景。对于已经加载出来的页面,可以用online/offline窗口事件做一个轻量的连接状态提示(原文见 rule.md 的 Detecting Online/Offline State in the UI):

// Notify users when connection is lost or restored function setupConnectivityBanner() { const banner = document.createElement('div') banner.setAttribute('role', 'status') banner.setAttribute('aria-live', 'polite') banner.style.cssText = ` position: fixed; bottom: 1rem; left: 50%; transform: translateX(-50%); background: #1e293b; color: #fff; padding: 0.5rem 1.25rem; border-radius: 2rem; font-size: 0.875rem; display: none; z-index: 9999; ` document.body.appendChild(banner) function showBanner(message: string) { banner.textContent = message banner.style.display = 'block' } function hideBanner() { banner.style.display = 'none' } window.addEventListener('offline', () => showBanner('You are offline')) window.addEventListener('online', () => { showBanner('Back online') setTimeout(hideBanner, 3000) }) } if (typeof window !== 'undefined') { setupConnectivityBanner() }

实现上有两个细节值得保留:横幅使用role="status"aria-live="polite",让屏幕阅读器在连接状态变化时自动播报;typeof window !== 'undefined'守卫使同一段代码可以安全地放在会被服务端渲染的执行路径中。

Workbox 方案:setCatchHandler 自动兜底

如果使用 Workbox,offlineFallback相关的样板逻辑可以被precacheAndRoutesetCatchHandler的组合替代(原文见 rule.md 的 Using Workbox):

// sw.ts declare const self: ServiceWorkerGlobalScope & { __WB_MANIFEST: unknown[] } precacheAndRoute(self.__WB_MANIFEST) // /offline must be in the manifest registerRoute( ({ request }) => request.mode === 'navigate', new NetworkFirst({ cacheName: 'pages' }) ) // Catch all failed navigation requests setCatchHandler(async ({ request }) => { if (request.destination === 'document') { return (await caches.match('/offline'))! } return Response.error() })

要点:

  • /offline必须出现在构建工具注入的__WB_MANIFEST预缓存清单中,否则caches.match('/offline')会取不到;
  • setCatchHandler是路由失败(含网络不可达)后的统一落点,用request.destination === 'document'精确圈定“这是文档级导航”;
  • 对非文档请求直接Response.error(),避免把离线页 HTML 喂给图片、脚本等请求。

仓库的 service-worker 规则 还展示了 Workbox 的完整生产形态:静态资源走CacheFirst并挂ExpirationPlugin(30 天过期)、/api/NetworkFirst、页面走StaleWhileRevalidate,以及 Next.js 下通过next-pwa配置runtimeCaching的集成方式——离线回退页在这套体系中只需确保它进入预缓存清单即可。

验证清单:如何确认离线回退页真正生效

规则文档将验证分为自动化与手动两组(原文见 rule.md 的 Verification 一节),均可直接照做:

自动化检查

  1. 注册 Service Worker 后,在 DevTools →ApplicationService Workers中确认其处于激活状态;
  2. 在 Network 面板把网络状态切换为Offline,然后访问一个不在缓存中的页面——应该看到你的自定义离线页,而不是浏览器默认错误屏;
  3. 运行 Lighthouse PWA 审计,确认 “Responds with a 200 when offline” 检查项通过。

手动检查

  • Cache Storage面板中,确认/offline出现在你的缓存名(如static-v1)之下。

另有一条来自 Support Notes 的工程提醒:离线行为依赖浏览器对 Service Worker、Cache Storage 与可安装性的实际支持,应在受支持的多浏览器上验证,而不是只在单一开发环境验证,并显式记录对不支持浏览器的优雅降级方案。从仓库中 service-worker 规则的 Support Notes 看,该类能力的基线兼容参考为 chrome 115、edge 115、firefox 116、safari 16.4、safari_ios 16.4,当你的目标浏览器矩阵低于此范围时应额外提供降级说明。

代码审查要点小结

落地这条规则后,可以用codeReview提示中的三个问题做最终把关:

  1. 离线页是否在install事件中通过addAll预缓存?
  2. 离线页是否真的零外部依赖(内联样式、系统字体、无外部脚本/图片)?
  3. 回退是否只对mode === 'navigate'(或destination === 'document')的请求生效,而不会把 HTML 返回给子资源请求?

这三点分别对应离线页“拿得到”“打得开”“不串味”三类最常见的线上事故。完整规则文案可回溯至 packages/content/rules/en/performance/offline-fallback.mdx,配套的 Agent 技能入口为 skills/offline-fallback/SKILL.md,其中声明了该技能的触发场景——“为站点添加 PWA 能力、实现 Service Worker、或改进不可靠网络下用户体验时使用”。

【免费下载链接】Front-End-Checklist🗂 The essential checklist for modern web development, for humans and AI agents项目地址: https://gitcode.com/gh_mirrors/fr/Front-End-Checklist

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询