你如何确保服务工作者缓存一组一致的文件?

How do you ensure a service worker caches a consistent set of files?

我有一个渐进式 Web 应用程序 (PWA),由多个文件组成,包括 index.htmlmanifest.jsonbundle.jsserviceWorker.js。我通过将所有这些文件上传到我的主机来更新我的应用程序。以防万一,我使用的是 Firebase,所以我使用 firebase deploy 上传文件。

通常一切正常:当现有用户打开应用程序时,他们仍然看到旧版本,但在后台,新服务人员 install 将任何更改的文件保存到缓存中。然后,当用户下次打开应用程序时,它 activates 并且他们会看到新版本。

但是当用户在我部署后不久打开应用程序时出现问题。似乎发生的情况是:主机提供新的 serviceWorker.js 而旧的 bundle.js。因此 install 将旧 bundle.js 放入其新缓存中。用户获得旧功能或更糟的是可能获得由新旧文件不一致混合组成的应用程序。

我想有人会说这是主机的错,没有自动更新,但我无法控制 Firebase。无论如何这听起来是不可能的,因为浏览器正在发送一系列独立的提取,并且不能保证它们都是 return 一致的版本。

如果有帮助,这是我的serviceWorker.jscacheName 字符串,例如 "app1-a0f43550e414" 是由我的构建管道生成的。这里 a0f43550e414 是最新的 bundle.js 的哈希值,因此仅当 bundle.js 的内容发生变化时才会更新缓存。

"use strict";
const appName = "app1";
const cacheLookup = {
    "app1-aefa820f62d2": "/",
    "app1-a0f43550e414": "bundle.js",
    "app1-23d94a4a7388": "manifest.json"
};
self.addEventListener("install", function (event) {
    event.waitUntil(
        Promise.all(Object.keys(cacheLookup).map(cacheName =>
            caches.open(cacheName)
                .then(cache => cache.add(cacheLookup[cacheName]))
        ))
    );
});
self.addEventListener("activate", event => {
    event.waitUntil(
        caches.keys().then(cacheNames =>
            Promise.all(cacheNames.map(cacheName => {
                if (cacheLookup[cacheName]) {
                    // cacheName holds a file still needed by this version
                } else if (!cacheName.startsWith(appName + "-")) {
                    // Do not delete the cache of other apps at same scope
                } else {
                    console.log("Deleting out of date cache:", cacheName);
                    return caches.delete(cacheName);
                }
            }))
        )
    );
});
const handleCacheMiss = request =>
    new Promise((_, reject) => {
        reject(Error("Not in service worker cacheLookup: " + request.url));
    });
self.addEventListener("fetch", event => {
    const request = event.request;
    event.respondWith(
        caches.match(request).then(cachedResponse =>
            cachedResponse || handleCacheMiss(request)
        )
    );
});

我考虑过将我所有的 HTML、CSS 和 JavaScript 捆绑到一个巨大的文件中,这样就不会出现不一致的情况。但是 PWA 需要几个无法捆绑的支持文件,包括 service worker、清单和图标。如果我尽可能地捆绑,用户仍然会被旧版本的捆绑包困住,并且仍然有不一致的支持文件。无论如何,将来我想通过减少捆绑和拥有更多文件来增加粒度,这样在典型的更新中只需要获取几个小文件。

我也考虑过上传 bundle.js 和其他每个版本具有不同文件名的文件。 service worker 的 fetch 可以隐藏名称更改,因此 index.html 等其他文件仍然可以将其称为 bundle.js。但我不明白这是如何在浏览器第一次加载应用程序时工作的。而且我认为您不能重命名 index.htmlmanifest.json.

听起来您在 install 处理程序中对 bundle.js 的请求可能由 HTTP 缓存而不是通过网络来完成。

您可以尝试更改此当前代码段:

cache.add(cacheLookup[cacheName])

显式创建 Request 对象,将其 cache mode 设置为 reload 以确保 HTTP 缓存不提供响应:

cache.add(new Request(cacheLookup[cacheName], {cache: 'reload'})

或者,如果您担心非原子全局部署并且您可以努力生成 sha256 或更好的哈希作为构建过程的一部分,您可以使用 subresource integrity 以确保您从网络中获得新服务工作者所期望的正确响应字节。

调整您的代码将类似于以下内容,您实际上必须在构建期间为您关心的每个文件生成正确的 sha256 哈希值:

const cacheLookup = {
    "sha256-[hash]": "/",
    "sha256-[hash]": "bundle.js",
    "sha256-[hash]": "manifest.json"
};

// Later...
cache.add(new Request(cacheLookup[cacheName], {
  cache: 'reload',
  integrity: cacheName,
}));

如果 SRI 不匹配,则对给定资源的请求将失败,这将导致 cache.add() 拒绝,进而导致整个 Service Worker 安装失败。下次 update check 发生时将重试服务工作者安装,届时(希望!)部署将完成并且 SRI 将有效。