XHR 请求的 Workbox 离线回退

Workbox offline fallback for XHR Requests

我正在使用 NextJS 和 Workbox 创建 PWA 和我需要的离线支持:https://github.com/shadowwalker/next-pwa。上面的回购协议中有一个我需要的例子:离线回退。我不需要应用程序在离线模式下完全工作,只需要一个指示连接丢失的后备页面。

我阅读了有关综合回退的工作箱部分:https://developers.google.com/web/tools/workbox/guides/advanced-recipes#comprehensive_fallbacks

有一个 catchHandler,它会在任何其他路由无法生成响应时触发,但问题是我在捕获 XMLHttpRequests (XHR) 错误时遇到了很大的麻烦。

例如,当客户端将请求发送到 API 时,如果没有互联网连接,我想改为呈现后备页面。如果失败的请求是“文档”,则处理程序仅提供回退页面,并且由于 XHR 请求不是文档,所以我无法处理它们。

import { clientsClaim } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import {
  NetworkOnly,
  NetworkFirst,
  StaleWhileRevalidate,
} from 'workbox-strategies';
import {
  registerRoute,
  setDefaultHandler,
  setCatchHandler,
} from 'workbox-routing';
import {
  precacheAndRoute,
  cleanupOutdatedCaches,
  matchPrecache,
} from 'workbox-precaching';

clientsClaim();

// must include following lines when using inject manifest module from workbox
// https://developers.google.com/web/tools/workbox/guides/precache-files/workbox-build#add_an_injection_point
const WB_MANIFEST = self.__WB_MANIFEST;
// Precache fallback route and image
WB_MANIFEST.push({
  url: '/fallback',
  revision: '1234567890',
});

cleanupOutdatedCaches();

precacheAndRoute(WB_MANIFEST);

registerRoute(
  '/',
  new NetworkFirst({
    cacheName: 'start-url',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 1,
        maxAgeSeconds: 86400,
        purgeOnQuotaError: !0,
      }),
    ],
  }),
  'GET'
);

// disable image cache, so we could observe the placeholder image when offline
registerRoute(
  /\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,
  new NetworkOnly({
    cacheName: 'static-image-assets',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 64,
        maxAgeSeconds: 86400,
        purgeOnQuotaError: !0,
      }),
    ],
  }),
  'GET'
);

registerRoute(
  /\.(?:js)$/i,
  new StaleWhileRevalidate({
    cacheName: 'static-js-assets',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 32,
        maxAgeSeconds: 86400,
        purgeOnQuotaError: !0,
      }),
    ],
  }),
  'GET'
);

registerRoute(
  /\.(?:css|less)$/i,
  new StaleWhileRevalidate({
    cacheName: 'static-style-assets',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 32,
        maxAgeSeconds: 86400,
        purgeOnQuotaError: !0,
      }),
    ],
  }),
  'GET'
);

registerRoute(
  /\.(?:json|xml|csv)$/i,
  new NetworkFirst({
    cacheName: 'static-data-assets',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 32,
        maxAgeSeconds: 86400,
        purgeOnQuotaError: !0,
      }),
    ],
  }),
  'GET'
);

registerRoute(
  /https:\/\/api[a-z-]*\.pling\.net\.br.*$/i,
  new NetworkFirst({
    cacheName: 'pling-api',
    networkTimeoutSeconds: 10,
    plugins: [
      new ExpirationPlugin({
        maxEntries: 16,
        maxAgeSeconds: 86400,
        purgeOnQuotaError: !0,
      }),
    ],
  }),
  'GET'
);

registerRoute(
  /https:\/\/[a-zA-Z0-9]+\.cloudfront.net\/.*$/i,
  new NetworkFirst({
    cacheName: 'cloudfront-assets',
    networkTimeoutSeconds: 10,
    plugins: [
      new ExpirationPlugin({
        maxEntries: 32,
        maxAgeSeconds: 86400,
        purgeOnQuotaError: !0,
      }),
    ],
  }),
  'GET'
);

registerRoute(
  /.*/i,
  new NetworkFirst({
    cacheName: 'others',
    networkTimeoutSeconds: 10,
    plugins: [
      new ExpirationPlugin({
        maxEntries: 32,
        maxAgeSeconds: 86400,
        purgeOnQuotaError: !0,
      }),
    ],
  }),
  'GET'
);

setDefaultHandler(new NetworkOnly());

// This "catch" handler is triggered when any of the other routes fail to
// generate a response.
setCatchHandler(({ event }) => {
  switch (event.request.destination) {
    case 'document':
      // If using precached URLs:
      return matchPrecache('/fallback');
    case 'image':
      return matchPrecache('/static/images/fallback.png');
    default:
      // If we don't have a fallback, just return an error response.
      // Switch statement for XHR Requests  
      return Response.error();
  }
});

您描述的场景——源自已加载页面的失败 XHR 应触发“错误页面”——最好通过 window 上下文中的客户端代码解决,而不是通过服务工作者逻辑。我认为这更符合 Service Worker 的“本意”使用方式,并且会带来更好的用户体验。

执行此操作的代码类似于;

const xhrRequest = new XMLHttpRequest();
// Set request details and make the request.

xhrRequest.addEventListener('error', (event) => {
  // Do something to display a "Sorry, an error occurred."
  // message within your open page.
});

因此,与其在 XHR 失败时尝试加载完全不同的页面,不如在现有页面的某处显示错误消息。 (如何显示此消息的详细信息取决于您一般如何处理页面的 UI。)

如果您真的想在 XHR 失败时用专用错误页面完全替换当前页面,那么在 error 事件侦听器中,您可以做一个window.location.href = '/offline.html'.

如果你真的真的想为此使用服务人员(出于某种原因;我认为你不应该),理论上您可以在基于 Workbox 的服务工作者中使用 Clients API 来强制导航:

setCatchHandler(async ({ event }) => {
  switch (event.request.destination) {
    case 'document':
      // If using precached URLs:
      return matchPrecache('/fallback');
    case 'image':
      return matchPrecache('/static/images/fallback.png');
    default:
      if (event.request.url === 'https://example.com/api') {
        // See https://developer.mozilla.org/en-US/docs/Web/API/WindowClient/navigate
        const client = await self.clients.get(event.clientId);
        await client.navigate('/offline.html');
      }

      // If we don't have a fallback, just return an error response.
      // Switch statement for XHR Requests  
      return Response.error();
  }
});