Service Worker 缓存策略

到目前为止,有关 Cache 接口的内容也只是提及和微小的代码段。为了有效地使用 Service Worker,您需要采用一种或多种缓存策略,这需要对 Cache 接口有一定的了解。

缓存策略是 Service Worker 的 fetch 事件与 Cache 接口之间的交互。缓存策略的编写方式取决于;例如,可能更可取方式是采用与文档不同的方式处理静态资源请求,这会影响缓存策略的组合方式。

在开始介绍这些策略之前,我们先花点时间探讨一下什么是 Cache 接口,它是什么,以及它提供的一些用于管理 Service Worker 缓存的方法。

Cache 接口与 HTTP 缓存

如果您以前没有使用过 Cache 接口,可能会倾向于将其视作与 HTTP 缓存相同或至少与 HTTP 缓存相关。事实并非如此。

  • Cache 接口是一种与 HTTP 缓存完全独立的缓存机制。
  • 无论您使用哪种 Cache-Control 配置来影响 HTTP 缓存,都不会影响存储在 Cache 接口中的资源。

不妨将浏览器缓存视为分层。HTTP 缓存是一种低级别缓存,由键值对驱动,其指令以 HTTP 标头表示。

相比之下,Cache 接口是由 JavaScript API 驱动的高级缓存。与使用相对简单的 HTTP 键值对相比,这种方法具有更大的灵活性,并且是实现缓存策略的一半。围绕 Service Worker 缓存的一些重要的 API 方法包括:

这些仅仅是几个。还有其他一些有用的方法,但这些方法将在本指南的后面部分介绍的基本方法。

不起眼的 fetch 活动

缓存策略的另一半是 Service Worker 的 fetch 事件。到目前为止,在本文档中,您已经听说过关于“拦截网络请求”的一些知识,并会在 Service Worker 内的 fetch 事件中发生以下情况:

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('install', (event) => {
  event.waitUntil(caches.open(cacheName));
});

self.addEventListener('fetch', async (event) => {
  // Is this a request for an image?
  if (event.request.destination === 'image') {
    // Open the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Respond with the image from the cache or from the network
      return cache.match(event.request).then((cachedResponse) => {
        return cachedResponse || fetch(event.request.url).then((fetchedResponse) => {
          // Add the network response to the cache for future visits.
          // Note: we need to make a copy of the response to save it in
          // the cache and use the original as the request response.
          cache.put(event.request, fetchedResponse.clone());

          // Return the network response
          return fetchedResponse;
        });
      });
    }));
  } else {
    return;
  }
});

这是一个玩具示例,您也可以亲眼见证其实际应用场景,但它可以让您大致了解 Service Worker 可以做什么。上述代码会执行以下操作:

  1. 检查请求的 destination 属性,查看这是否为图片请求。
  2. 如果图片位于 Service Worker 缓存中,则从那里提供。如果没有,请从网络中提取图片,将响应存储在缓存中,并返回网络响应。
  3. 所有其他请求均通过 Service Worker 传递,不与缓存交互。

提取的 event 对象包含一个 request 属性,该属性提供了一些有用的信息,可帮助您识别每个请求的类型:

  • url:当前由 fetch 事件处理的网络请求的网址。
  • method:这是请求方法(例如,GETPOST)。
  • mode:描述请求的模式。值 'navigate' 通常用于区分对 HTML 文档的请求与其他请求。
  • destination:用于描述所请求的内容类型,并避免使用所请求资产的文件扩展名。

再次强调一下,asynchrony 是游戏的名称。您应该记得,install 事件提供 event.waitUntil 方法,该方法接受 promise 并等待其解决,然后继续执行激活。fetch 事件提供类似的 event.respondWith 方法,您可以使用该方法返回异步 fetch 请求的结果或 Cache 接口的 match 方法返回的响应。

缓存策略

现在,您已对 Cache 实例和 fetch 事件处理脚本有一定的了解,可以深入了解一些 Service Worker 缓存策略了。虽然有无穷无尽的可能性,但本指南将坚持 Workbox 随附的策略,以便您了解 Workbox 的内部运作方式。

仅限缓存

显示从页面到 Service Worker 再到缓存的流。

我们先从一个简单的缓存策略开始,我们将其称为“仅缓存”。只是:当 Service Worker 控制着页面时,匹配的请求将只会进入缓存。这意味着,任何缓存的资源都需要进行预缓存,以使模式正常工作,并且在 Service Worker 更新之前,绝不会在缓存中更新这些资源。

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

// Assets to precache
const precachedAssets = [
  '/possum1.jpg',
  '/possum2.jpg',
  '/possum3.jpg',
  '/possum4.jpg'
];

self.addEventListener('install', (event) => {
  // Precache assets on install
  event.waitUntil(caches.open(cacheName).then((cache) => {
    return cache.addAll(precachedAssets);
  }));
});

self.addEventListener('fetch', (event) => {
  // Is this one of our precached assets?
  const url = new URL(event.request.url);
  const isPrecachedRequest = precachedAssets.includes(url.pathname);

  if (isPrecachedRequest) {
    // Grab the precached asset from the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      return cache.match(event.request.url);
    }));
  } else {
    // Go to the network
    return;
  }
});

如上所示,一组资源是在安装时预缓存的。在 Service Worker 处理提取内容时,我们会检查由 fetch 事件处理的请求网址是否在预缓存的资源数组中。如果是,我们从缓存中获取资源并跳过网络。其他请求会传递到网络,且仅传递到网络。 如需了解此策略的实际运用,请观看此演示,并在您的控制台打开的情况下进行查看。

仅限网络

显示从页面到 Service Worker 再到网络的流。

与“仅缓存”相反,“仅限网络”是指请求通过 Service Worker 传递到网络,而无需与 Service Worker 缓存进行任何交互。这是确保内容新鲜度的好策略(比如使用标记),但需要权衡的是,用户离线时自定义设置将永远不会起作用。

确保请求传递到网络只是表示您不会为匹配的请求调用 event.respondWith。如果您想明确指定此类请求,可以在 fetch 事件回调中针对要传递到网络的请求设置一个空的 return;。这就是“仅缓存”策略演示中针对非预缓存请求的具体情况。

缓存优先,回退到网络

显示从页面到 Service Worker,再到缓存,然后到网络(如果不在缓存中)的流程。

在此策略中,需要更深入地发挥作用。对于匹配请求,流程如下:

  1. 请求到达缓存。如果资源位于缓存中,请从缓存中提供。
  2. 如果请求不在缓存中,请转到网络。
  3. 网络请求完成后,将其添加到缓存中,然后从网络返回响应。

下面是此策略的示例,您可以在实时演示中进行测试:

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  // Check if this is a request for an image
  if (event.request.destination === 'image') {
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Go to the cache first
      return cache.match(event.request.url).then((cachedResponse) => {
        // Return a cached response if we have one
        if (cachedResponse) {
          return cachedResponse;
        }

        // Otherwise, hit the network
        return fetch(event.request).then((fetchedResponse) => {
          // Add the network response to the cache for later visits
          cache.put(event.request, fetchedResponse.clone());

          // Return the network response
          return fetchedResponse;
        });
      });
    }));
  } else {
    return;
  }
});

虽然此示例仅涵盖图片,但这是一项适用于所有静态资源(例如 CSS、JavaScript、图片和字体)的绝佳策略,尤其是经过哈希处理的资源。 它可以绕过 HTTP 缓存可能启动的服务器执行任何内容新鲜度检查,从而加快不可变资源的速度。更重要的是,所有缓存的资源都将离线可用。

网络优先、回退到缓存

显示从网页到 Service Worker,再到网络,再到缓存的流程(如果网络不可用)。

如果您打算去掉“先缓存,第二网络再缓存”策略,最终就会采用“网络先缓存,然后再缓存”策略,如下所示:

  1. 您先前往网络请求一个请求,然后将响应放入缓存中。
  2. 如果您日后处于离线状态,则会回退到缓存中该响应的最新版本。

此策略非常适合 HTML 或 API 请求,当您想在线获取资源的最新版本,同时希望离线访问最新的可用版本时。以下是应用于 HTML 请求时可能如下所示:

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  // Check if this is a navigation request
  if (event.request.mode === 'navigate') {
    // Open the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Go to the network first
      return fetch(event.request.url).then((fetchedResponse) => {
        cache.put(event.request, fetchedResponse.clone());

        return fetchedResponse;
      }).catch(() => {
        // If the network is unavailable, get
        return cache.match(event.request.url);
      });
    }));
  } else {
    return;
  }
});

您可以在演示中尝试此操作。 首先,进入相应页面。在将 HTML 响应放入缓存之前,您可能需要重新加载。 然后,在开发者工具中模拟离线连接,并再次重新加载。 系统会立即从缓存中提供上一个可用的版本。

如果离线功能很重要,但您需要在该功能与访问最新版本的标记或 API 数据之间取得平衡,则“网络优先,缓存次之”是实现该目标的可靠策略。

重新验证时过时

显示从页面到 Service Worker,再到缓存,再从网络到缓存的流。

在我们到目前为止所介绍的策略中,“Stale-while-revalidate”最为复杂。从某些方面来看,该机制与最后两种策略类似,但其过程优先考虑资源访问速度,同时还在后台保持更新。策略大致如下:

  1. 在第一次请求获取资源时,从网络中提取资源,将其放入缓存中并返回网络响应。
  2. 对于后续请求,首先从缓存提供资源,然后“在后台”从网络重新请求该资源,并更新资源的缓存条目。
  3. 对于此后的请求,您将收到在上一步中从缓存中放置的最后一个网络提取的版本。

对于很重要、但并非很重要的事情,这是一种很好的策略。 可以将内容想象成社交媒体网站的头像。 它们会在用户执行相应操作时进行更新,但并不是每个请求都绝对有必要使用最新版本。

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  if (event.request.destination === 'image') {
    event.respondWith(caches.open(cacheName).then((cache) => {
      return cache.match(event.request).then((cachedResponse) => {
        const fetchedResponse = fetch(event.request).then((networkResponse) => {
          cache.put(event.request, networkResponse.clone());

          return networkResponse;
        });

        return cachedResponse || fetchedResponse;
      });
    }));
  } else {
    return;
  }
});

您可以在另一个实时演示中查看实际操作,尤其是注意浏览器开发者工具中的“网络”标签页及其 CacheStorage 查看器(如果浏览器的开发者工具包含此类工具)时。

转向 Workbox!

本文总结了我们对 Service Worker 的 API 以及相关 API 的回顾,这意味着您已充分了解如何直接使用 Service Worker 着手对 Workbox 进行修补!