如何确保 Workbox 缓存更新永不激活的更新

How to ensure Workbox cache update for updates that never activate

我一直致力于将我们必须的 Web 应用程序转换为 PWA,并且我正在使用 Google 的 Workbox (v3.6.1) 来预缓存资源。在大多数情况下,它运行良好,但似乎有一个特定的场景导致缓存文件不同步。

我正在使用基本 precacheAndRoute() 功能来设置文件以进行预缓存。

workbox.precaching.precacheAndRoute([]); //populated at build time via workbox-cli

在首次安装和大多数更新时,文件会按预期进行预缓存。但是,如果 Service Worker 实例当前 waiting 并且安装了新更新,则临时缓存中的所有待处理文件都将被删除并且不会安装最新版本。

似乎 workbox.precaching 的安装步骤在将文件添加到临时缓存时更新了包含所有文件版本的 IndexDB。因此,下一个 Service Worker 版本认为所有文件的先前最新版本当前都已缓存,即使它们仍然仅在临时缓存中。然后新安装在插入它自己的文件之前从临时缓存中删除所有内容。因此,前一个 waiting 实例的未决缓存文件将永远丢失。

我的想法是,在安装新版本时,我可以强制临时缓存同步到永久缓存(通过使用 PrecacheControlleractivate() 函数),然后再允许要预缓存的新实例,但我对在用户积极使用应用程序时更新永久缓存有一些担忧。

我正在寻找确认我的想法是一个合适的解决方案,或者关于如何处理这种情况的任何其他建议。

Workbox 最近发布了 4.0.0 版,该问题似乎已通过该升级得到解决。我将保留以下答案,因为它可能对目前无法升级到 4.0.0 的任何人仍然有用。


正如我在问题中提到的那样,我使用 PrecacheController 或多或少地实现了这个功能。一个令人讨厌的部分是我必须自己实现 fetchactivate 监听器,因为我不再使用标准 workbox.precaching。如果有人有任何其他想法,请随时 post 其他选项。

const precacheController = new workbox.precaching.PrecacheController();
precacheController.addToCacheList([]); //populated at build-time with workbox-cli

self.addEventListener('fetch', (event) => {
    var url = event.request.url.endsWith('/') ? event.request.url + 'index.html' : event.request.url;
    event.respondWith(new Promise(function (resolve) {
            if (precacheController.getCachedUrls().indexOf(url) > -1) {
                resolve(caches.open(workbox.core.cacheNames.precache)
                    .then((cache) => {
                        return cache.match(url);
                    })
                    .then((cachedResponse) => {
                        return cachedResponse || fetch(url);
                    }));
            } else {
                resolve(fetch(event.request));
            }
        }));
});

self.addEventListener('activate', (event) => {
    event.waitUntil(precacheController.activate());
    self.clients.claim();
});

self.addEventListener('install', function (event) {
    var timeoutId = null;
    event.waitUntil((new Promise(function (resolve, reject) {
                if (self.registration.waiting) {
                    var channel = new MessageChannel();
                    channel.port1.onmessage = function (event) {
                        resolve();
                    };

                    //tell the current 'waiting' instance to cleanup its temp cache
                    self.registration.waiting.postMessage({
                        action: 'cleanupCache'
                    }, [channel.port2]);
                } else {
                    resolve();
                }
            }))
        .finally(function () {
            //once temp cache is cleaned up from any 'waiting' instance, begin my install
            return precacheController.install();
        }));
});

self.addEventListener('message', function (event) {
    if (event.data.action === 'cleanupCache') {
    //move files from temp cache to permanent cache
        precacheController.activate().finally(function () {
            if (event.ports[0]) {
                event.ports[0].postMessage('cleanupComplete');
            }
        });
    }
});