管理 Service Worker 缓存

7
我目前正在尝试的服务工作者代码部分如下所示:
self.addEventListener('install', function(event) {
    event.waitUntil(
        caches.open('v1').then(function(cache) {
            return cache.addAll([
                '/react-redux/node_modules/react/dist/react-with-addons.js',
                '/react-redux/node_modules/react-dom/dist/react-dom.js',
                '/react-redux/a.js'
            ]);
        })
    );
});

当然,使用标准的fetch事件监听器从缓存中返回,或者如果该项不存在,则运行网络请求。
但是,如果仅更新了上面的示例中的a.js文件,如何让service worker更新该文件,而更新该文件;并且如何确保用户在下次浏览我的页面时不会从service worker获取现在已经过时的文件版本?
我能想到的最好的方法是为这些文件url添加缓存破坏器,例如
'/react-redux/node_modules/react/dist/react-with-addons.js?hash=1MWRF3...'
然后使用相同的当前哈希/缓存破坏器更新任何模块加载器,然后在SW安装事件中迭代当前缓存键,删除任何过时的内容,并添加任何缺失的内容。
似乎解决了两个问题:当文件更新时,发送的网络请求将不匹配现在过时的Service Worker中的任何内容,因此将发生相同的网络回退;以及在Service Worker的安装事件中进行选择性缓存插入时,不会尝试向已存在且当前的缓存中添加内容。
当然,随着这些哈希值的更改(自动来自构建过程),Service Worker代码也会更改,因此在文件更改时重新安装SW也会发生。
但是我不禁想,是否有更简单的方法呢?
2个回答

6
你对于理想情况下的处理方式以及确保缓存资源高效可靠更新的困难有着准确的理解。
虽然你可以自己编写方法,但是已经存在的工具可以自动完成对每个文件进行指纹识别并生成一个服务工作者文件来管理你的缓存资源。我开发了其中之一,名为 sw-precache。另一个类似的选择是 offline-plugin

非常出色 - 我会查看那些资源。非常感谢。那么...你认识/与Jake Archibald合作过吗? :) - Adam Rackis
3
是的,我确实有荣幸成为杰克的同事! - Jeff Posnick
那么 sw-precache - 它如何与客户端/模块加载集成呢?您可能正在使用 Webpack、SystemJS 或其他选项 - sw-precache 是否输出正确的 URL 以供使用,并由您负责将它们与客户端加载程序同步? - Adam Rackis
sw-precache 在构建完成后会查看本地存在的文件名,并使用基于这些文件名的 URL 来填充其缓存。通常情况下,不需要特别调整客户端加载器。如果您遇到问题,请通过 https://github.com/GoogleChrome/sw-precache/issues 告知我们。 - Jeff Posnick
1
一般来说,使用缓存优先策略意味着在下一次访问时所有内容都会“过期”,更新仅在第N+1次访问时可见。检测 SW 更新,然后显示“请重新加载...”烤面包提示消息是当前的 UX 最佳实践。 - Jeff Posnick
显示剩余5条评论

1
我最终编写了与您所说完全相同的代码,以下是代码,供那些自己编写时遇到困难的人使用:
首先,我们需要编写代码,在每次捆绑文件更改时向捆绑文件的URL添加时间戳/哈希值。
我们大多数人使用webpack进行应用程序的捆绑,并且每次执行webpack配置文件时,捆绑似乎都会更改,因此我们将在此处插入哈希/时间戳。我有一个名为index.template.html的文件,其中存储了提供给用户的文件,因此要修改URL,我做了以下操作:
// webpack.config.js

const webpack = require('webpack');
const fs = require('fs');

fs.readFile('./public/index.template.html', function (err, data) {
    if (err) return console.log('Unable to read index.template file', err);
    fs.writeFile('./public/index.template.html',
        // finding and inserting current timestamp in front of the URL for cache busting
        data.toString('utf8').replace(/bundle\.js.*"/g, "bundle\.js\?v=" + Math.floor(Date.now() / 1000) + "\""),
        (err) => {
            if (err) console.log("Unable to write to index.template.html", err);
        });
});

module.exports = {
    // configuration for webpack
};

现在这里是服务工作者的代码,它检测URL的变化并重新获取和替换缓存中的资源以防发生更改,我已经尝试在注释中解释其工作原理:
self.addEventListener("fetch", function (event) {
    event.respondWith(
        // intercepting response for bundle.js since bundle.js may change and we need to replace it in our cahce
        event.request.url.indexOf('public/bundle.js') != -1 ?
        checkBundle(event.request) : //if it is the bundle URL then use our custom function for handling the request
        caches.match(event.request) //if its not then do the use service-worker code:
            .then(function(response) {
                // other requests code
            })
        );
});

// our custom function which does the magic:
function checkBundle(request) {
    return new Promise(function(resolve, reject){ // respondWith method expects a Promise
        caches.open(cacheName).then(function(cache) {
             //first lets check whether its in cache already or not
             // ignoreSearch parameter will ignore the query parameter while searching in cache, i.e., our cache busting timestmap
            cache.keys(request, { ignoreSearch: true }).then(function(keys) {    
                if(keys.length == 0) {
                    // its not in cache so fetch it
                    return resolve(fetch(request).then(
                        function (response) {
                            if (!response || (response.status !== 200 && response.status !== 0)) {
                                return response;
                            }                  
                            cache.put(request, response.clone());                           
                            return response;
                        }
                    ));
                }
                //it is in cache, so now we extract timestamp from current and cached URL and compare them
                const lastVersion = /bundle.js\?v=(.*)$/.exec(keys[0].url)[1],
                    curVersion = /bundle.js\?v=(.*)$/.exec(request.url)[1];

                if(lastVersion == curVersion) // if timestamp is change that means no change in the resource
                    return resolve(cache.match(request)); //return the cached resource

                //bundle file has changed, lets delete it from cache first
                cache.delete(keys[0]);
                //now we fetch new bundle and serve it and store in cache
                var fetchRequest = request.clone();
                resolve(fetch(fetchRequest).then(
                    function (response) {
                        if (!response || (response.status !== 200 && response.status !== 0)) {
                            return response;
                        }                  
                        cache.put(request, response.clone());                           
                        return response;
                    }
                ));
              });
        });
    });
}

正如Jeff Posnick在其他答案的评论中提到的那样,通常这些类型的方法需要N+1次访问才能查看更新后的资源,但是这种方法不需要,因为资源会被重新获取并提供给客户端,同时也会被替换在缓存中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接