缓存

当用户打开并与网站交互时,网站需要的所有资源,包括 HTML、JavaScript、CSS、图像、字体以及应用程序显式请求的任何数据,都会通过发出 HTTP(S) 请求来检索。PWA 最基本的功能之一是能够在设备上显式缓存应用程序的一些资源,这意味着可以检索这些资源而无需向网络发送请求。

在本地缓存资源主要有两种好处:**离线操作**和**响应速度**。

  • **离线操作**:缓存使 PWA 能够在设备没有网络连接的情况下在一定程度上运行。
  • **响应速度**:即使设备在线,如果 PWA 的用户界面是从缓存而不是网络获取的,其响应速度通常会快得多。

当然,主要缺点是**新鲜度**:缓存不太适合需要保持最新状态的资源。此外,对于某些类型的请求,例如 POST 请求,缓存从来不适合。

这意味着您是否应该以及何时缓存资源很大程度上取决于所讨论的资源,PWA 通常会对不同的资源采用不同的策略。在本指南中,我们将了解 PWA 的一些常见缓存策略,并了解哪些策略适合哪些资源。

缓存技术概述

PWA 可以构建缓存策略的主要技术是 Fetch APIService Worker APICache API

Fetch API

Fetch API 定义了一个全局函数 fetch() 用于获取网络资源,以及 RequestResponse 接口,分别代表网络请求和响应。fetch() 函数接受 Request 或 URL 作为参数,并返回一个 Promise,该 Promise 解析为 Response

fetch() 函数可用于服务工作者以及主应用程序线程。

Service Worker API

服务工作者是 PWA 的一部分:它是一个单独的脚本,在它自己的线程中运行,与应用程序的主线程分离。

一旦服务工作者处于活动状态,那么无论何时应用程序请求服务工作者控制的网络资源,浏览器都会在服务工作者的全局范围内触发一个名为 fetch 的事件。此事件不仅针对主线程的显式 fetch() 调用触发,而且还会针对浏览器在页面导航后发出的加载页面和子资源(例如 JavaScript、CSS 和图像)的隐式网络请求触发。

通过监听 fetch 事件,服务工作者可以拦截请求并返回自定义的 Response。特别是,它可以返回本地缓存的响应,而不是始终访问网络,或者如果设备处于离线状态,则返回本地缓存的响应。

Cache API

Cache 接口为 Request/Response 对提供了持久性存储。它提供了一些方法来添加和删除 Request/Response 对,以及查找与给定 Request 匹配的缓存 Response。缓存可用于主应用程序线程和服务工作者:因此,一个线程可以在那里添加响应,而另一个线程可以检索它。

最常见的是,服务工作者将在其 installfetch 事件处理程序中将资源添加到缓存中。

何时缓存资源

PWA 可以随时缓存资源,但在实践中,有一些时间大多数 PWA 会选择缓存资源。

  • **在服务工作者的 install 事件处理程序中(预缓存)**:当服务工作者被安装时,浏览器会在服务工作者的全局范围内触发一个名为 install 的事件。此时,服务工作者可以预缓存资源,从网络获取它们并将它们存储在缓存中。

    **注意:**服务工作者的安装时间与 PWA 的安装时间不同。服务工作者的 install 事件会在服务工作者下载并执行后立即触发,这通常会在用户访问您的网站后立即发生。

    即使用户从未将您的网站安装为 PWA,其服务工作者也会被安装和激活。

  • **在服务工作者的 fetch 事件处理程序中**:当服务工作者的 fetch 事件触发时,服务工作者可能会将请求转发到网络并缓存生成的响应,无论缓存中是否已经包含响应,还是用更新的响应更新缓存响应。
  • **响应用户请求**:PWA 可能会明确邀请用户下载资源以备后用,当设备可能处于离线状态时。例如,音乐播放器可能会邀请用户下载曲目以备后用。在这种情况下,主应用程序线程可以获取资源并将响应添加到缓存中。尤其是在请求的资源很大的情况下,PWA 可能会使用 Background Fetch API,在这种情况下,响应将由服务工作者处理,服务工作者会将其添加到缓存中。
  • **定期**:使用 Periodic Background Sync API,服务工作者可以定期获取资源并缓存响应,以确保即使设备处于离线状态,PWA 也可以提供合理的最新响应。

缓存策略

缓存策略是一种算法,用于确定何时缓存资源、何时提供缓存资源以及何时从网络获取资源。在本节中,我们将总结一些常见的策略。

这不是一个详尽的列表:它只是为了说明 PWA 可以采用的方法类型。

缓存策略在离线操作、响应速度和新鲜度之间取得平衡。不同的资源对此有不同的要求:例如,应用程序的基本 UI 可能比较静态,而显示产品列表时可能需要获取最新数据。这意味着 PWA 通常会对不同的资源采用不同的策略,而单个 PWA 可能会使用此处描述的所有策略。

先缓存

在此策略中,我们将预缓存一些资源,然后仅对这些资源实施“先缓存”策略。也就是说

  • 对于预缓存的资源,我们将
    • 在缓存中查找资源,如果找到则返回资源。
    • 否则,访问网络。如果网络请求成功,则缓存资源以备下次使用。
  • 对于所有其他资源,我们将始终访问网络。

预缓存适用于 PWA 确定需要、在本应用程序版本中不会更改且需要尽快获取的资源。这包括应用程序的基本用户界面。如果它被预缓存,则应用程序的 UI 可以在启动时呈现,无需任何网络请求。

首先,服务工作者在其 install 事件处理程序中预缓存静态资源

js
const cacheName = "MyCache_1";
const precachedResources = ["/", "/app.js", "/style.css"];

async function precache() {
  const cache = await caches.open(cacheName);
  return cache.addAll(precachedResources);
}

self.addEventListener("install", (event) => {
  event.waitUntil(precache());
});

install 事件处理程序中,我们将缓存操作的结果传递到事件的 waitUntil() 方法中。这意味着,如果缓存因任何原因失败,则服务工作者的安装也会失败:反之,如果安装成功,则服务工作者可以确定资源已添加到缓存中。

fetch 事件处理程序如下所示

js
async function cacheFirst(request) {
  const cachedResponse = await caches.match(request);
  if (cachedResponse) {
    return cachedResponse;
  }
  try {
    const networkResponse = await fetch(request);
    if (networkResponse.ok) {
      const cache = await caches.open("MyCache_1");
      cache.put(request, networkResponse.clone());
    }
    return networkResponse;
  } catch (error) {
    return Response.error();
  }
}

self.addEventListener("fetch", (event) => {
  if (precachedResources.includes(url.pathname)) {
    event.respondWith(cacheFirst(event.request));
  }
});

我们通过调用事件的 respondWith() 方法返回资源。如果我们没有为给定请求调用 respondWith(),则请求将被发送到网络,就好像服务工作者没有拦截它一样。因此,如果请求未被预缓存,它只会访问网络。

当我们将 networkResponse 添加到缓存中时,我们必须克隆响应并将副本添加到缓存中,返回原始响应。这是因为 Response 对象是可流式传输的,因此可能只能读取一次。

您可能想知道为什么我们对预缓存的资源回退到网络。如果它们被预缓存,我们不能确定它们会存在于缓存中吗?原因是缓存可能被清除,无论是由浏览器还是由用户清除。尽管这种情况不太可能发生,但如果没有办法回退到网络,它将使 PWA 无法使用。请参阅 删除缓存数据

先缓存,带缓存刷新

“先缓存”的缺点是,一旦响应在缓存中,它就不会被刷新,直到安装了新版本的 service worker。

“缓存优先,并刷新缓存”策略,也称为“陈旧时重新验证”,与“缓存优先”类似,不同之处在于,即使缓存命中,我们也会始终向网络发送请求,并使用响应刷新缓存。这意味着我们获得了“缓存优先”的响应速度,但也获得了相当新的响应(只要请求频率合理)。

当响应速度很重要,而新鲜度有一定重要性但不那么重要时,这是一个不错的选择。

在此版本中,我们为除 JSON 之外的所有资源实现了“缓存优先,并刷新缓存”。

js
function isCacheable(request) {
  const url = new URL(request.url);
  return !url.pathname.endsWith(".json");
}

async function cacheFirstWithRefresh(request) {
  const fetchResponsePromise = fetch(request).then(async (networkResponse) => {
    if (networkResponse.ok) {
      const cache = await caches.open("MyCache_1");
      cache.put(request, networkResponse.clone());
    }
    return networkResponse;
  });

  return (await caches.match(request)) || (await fetchResponsePromise);
}

self.addEventListener("fetch", (event) => {
  if (isCacheable(event.request)) {
    event.respondWith(cacheFirstWithRefresh(event.request));
  }
});

请注意,我们异步更新缓存(在 then() 处理程序中),因此应用程序无需等待接收网络响应才能使用缓存的响应。

网络优先

我们将要介绍的最后一种策略是“网络优先”,它是缓存优先的逆向策略:我们尝试从网络检索资源。如果网络请求成功,我们返回响应并更新缓存。如果失败,我们尝试使用缓存。

这对于需要获取最新响应的请求很有用,但缓存资源总比没有好。消息应用程序的最近消息列表可能属于此类。

在下面的示例中,我们使用“网络优先”来获取应用程序“收件箱”路径下所有资源的请求。

js
async function networkFirst(request) {
  try {
    const networkResponse = await fetch(request);
    if (networkResponse.ok) {
      const cache = await caches.open("MyCache_1");
      cache.put(request, networkResponse.clone());
    }
    return networkResponse;
  } catch (error) {
    const cachedResponse = await caches.match(request);
    return cachedResponse || Response.error();
  }
}

self.addEventListener("fetch", (event) => {
  const url = new URL(event.request.url);
  if (url.pathname.match(/^\/inbox/)) {
    event.respondWith(networkFirst(event.request));
  }
});

仍然存在一些请求,对于这些请求,没有响应比可能过时的响应更好,并且只适合使用“仅网络”策略。例如,如果应用程序显示可用产品的列表,那么如果列表过时,用户会感到沮丧。

删除缓存数据

缓存的存储空间有限,如果超出限制,浏览器可能会驱逐应用程序的缓存数据。具体的限制和行为因浏览器而异:有关详细信息,请参阅 存储配额和驱逐标准。实际上,驱逐缓存数据是一个非常罕见的事件。用户也可以随时清除应用程序的缓存。

PWA 应该在服务工作者的 activate 事件中清理所有旧版本的缓存:当此事件触发时,服务工作者可以确定没有先前版本的 service worker 正在运行,因此不再需要旧的缓存数据。

另请参阅