如何确保缓存页面缓存了相应的资产?

How to make sure a cached page has its corresponding assets cached?

tl;dr. 我的 Service Worker 正在缓存不同版本的 HTML 页面和 CSS 文件。离线:因为我必须限制我正在缓存的文件数量,我如何才能确保对于缓存中的每个 HTML 页,它需要的版本化 CSS 文件也在缓存?我需要在某个时候删除旧的 CSS 文件,它们与 HTML 文件没有直接关系。


我正在尝试将传统网站变成 PWA(某种程度上),使用 Service Worker 实施缓存策略(我使用的是 Workbox,但问题应该更通才)。

我在浏览网站时缓存页面(网络优先策略),以便离线使用它们。
我还使用缓存优先策略缓存了有限数量的 CSS 和 JS 资源。指向它们的 URL 已经使用嵌入在文件名中的时间戳“缓存”了:例如 front.320472502.css。由于缓存清除技术已经到位,我只 need/want 在此缓存中保留少量资产。

这就是我遇到的问题。假设我缓存了引用 /front.123.css 的页面 /contact(因此也被缓存)。当我导航到其他页面时,CSS 在此期间发生了多次更改,我的 CSS 缓存现在可能只包含 /front.455.css/front.456.css。如果我现在要离线,尝试加载 /contact 将成功检索页面内容,但 CSS 将无法加载,因为它不再在缓存中,并且它会呈现一个无样式的内容页。

要么我将 CSS 的版本在缓存中保存 时间,这并不理想,要么我尝试 清除缓存 CSS 仅当任何缓存页面不需要它时 。但是你会怎么做呢?遍历缓存页面,寻找 front.123.css 字符串?

另一种解决方案可能是返回一个离线页面而不是一个无样式的内容页面,但我不确定它是否可行,因为工作人员在知道它会使用什么资产之前用 HTML 响应需要。

此处的“最佳”解决方案是使用预缓存(通过 Workbox,或通过其他生成 build-time 清单的方式),并确保所有 HTML 和子资源被缓存并自动过期。如果您可以预缓存所有内容,则不必担心版本不匹配或缓存未命中。

也就是说,预缓存 所有内容 并不总是一个可行的选择,如果您的网站依赖于大量动态的 server-rendered 内容,或者如果您有许多不同的 HTML 页面,或者如果您有更多种类的子资源,其中许多仅在页面子集上需要。

如果您想使用运行时缓存方法,我会推荐一种类似于“Smarter runtime caching of hashed assets”中描述的技术。它使用自定义 Workbox 插件来处理缓存过期并在网络不可用时为给定的子资源查找“best-effort”缓存匹配。推广该代码的主要困难是您需要为哈希使用一致的命名方案,并编写一些实用函数以编程方式将哈希 URL 转换为“基础”URL.

为了提供一些代码以及此答案,这里是我当前使用的插件版本。不过,您需要按照上述方法针对您的哈希方案对其进行自定义。

import {WorkboxPlugin} from 'workbox-core';
import {HASH_CHARS} from './path/to/constants';

function getOriginalFilename(hashedFilename: string): string {
  return hashedFilename.substring(HASH_CHARS + 1);
}

function parseFilenameFromURL(url: string): string {
  const urlObject = new URL(url);
  return urlObject.pathname.split('/').pop();
}

function filterPredicate(
  hashedURL: string,
  potentialMatchURL: string,
): boolean {
  const hashedFilename = parseFilenameFromURL(hashedURL);
  const hashedFilenameOfPotentialMatch =
    parseFilenameFromURL(potentialMatchURL);

  return (
    getOriginalFilename(hashedFilename) ===
    getOriginalFilename(hashedFilenameOfPotentialMatch)
  );
}

export const revisionedAssetsPlugin: WorkboxPlugin = {
  cachedResponseWillBeUsed: async ({cacheName, cachedResponse, state}) => {
    state.cacheName = cacheName;
    return cachedResponse;
  },

  cacheDidUpdate: async ({cacheName, request}) => {
    const cache = await caches.open(cacheName);
    const keys = await cache.keys();

    for (const key of keys) {
      if (filterPredicate(request.url, key.url) && request.url !== key.url) {
        await cache.delete(key);
      }
    }
  },

  handlerDidError: async ({request, state}) => {
    if (state.cacheName) {
      const cache = await caches.open(state.cacheName);
      const keys = await cache.keys();

      for (const key of keys) {
        if (filterPredicate(request.url, key.url)) {
          return cache.match(key);
        }
      }
    }
  },
};