Service-worker force update of new assets

后端 未结 3 1856
灰色年华
灰色年华 2020-12-05 00:13

I\'ve been reading through the html5rocks Introduction to service worker article and have created a basic service worker that caches the page, JS and CSS which works as expe

3条回答
  •  我在风中等你
    2020-12-05 00:36

    A browser caching issue

    The main problem here is that when your new service worker is installing, he fetches requests that are handled by the previous service worker and it's very likely that he's getting resources from cache because this is your caching strategy. Then even though you're updating your service worker with new code, a new cache name, calling self.skipWaiting(), he's still putting in cache the old resources in cache!

    This is how I fully update a Service Worker

    One thing to know is that a service worker will trigger the install event each time your code script changes so you don't need to use version stamps or anything else, just keeping the same file name is okay and even recommended. There are other ways the browser will consider your service worker updated.

    1. Rewrite your install event handler:

    I don't use cache.addAll because it is broken. Indeed if one and only one of your resource to cache cannot be fetched, the whole installation will fail and not even one single file will be added to the cache. Now imagine your list of files to cache is automatically generated from a bucket (it's my case) and your bucket is updated and one file is removed, then your PWA will fail installing and it should not.

    sw.js

    self.addEventListener('install', (event) => {
      // prevents the waiting, meaning the service worker activates
      // as soon as it's finished installing
      // NOTE: don't use this if you don't want your sw to control pages
      // that were loaded with an older version
      self.skipWaiting();
    
      event.waitUntil((async () => {
        try {
          // self.cacheName and self.contentToCache are imported via a script
          const cache = await caches.open(self.cacheName);
          const total = self.contentToCache.length;
          let installed = 0;
    
          await Promise.all(self.contentToCache.map(async (url) => {
            let controller;
    
            try {
              controller = new AbortController();
              const { signal } = controller;
              // the cache option set to reload will force the browser to
              // request any of these resources via the network,
              // which avoids caching older files again
              const req = new Request(url, { cache: 'reload' });
              const res = await fetch(req, { signal });
    
              if (res && res.status === 200) {
                await cache.put(req, res.clone());
                installed += 1;
              } else {
                console.info(`unable to fetch ${url} (${res.status})`);
              }
            } catch (e) {
              console.info(`unable to fetch ${url}, ${e.message}`);
              // abort request in any case
              controller.abort();
            }
          }));
    
          if (installed === total) {
            console.info(`application successfully installed (${installed}/${total} files added in cache)`);
          } else {
            console.info(`application partially installed (${installed}/${total} files added in cache)`);
          }
        } catch (e) {
          console.error(`unable to install application, ${e.message}`);
        }
      })());
    });
    

    2. Clean the old cache when the (new) service worker is activated:

    sw.js

    // remove old cache if any
    self.addEventListener('activate', (event) => {
      event.waitUntil((async () => {
        const cacheNames = await caches.keys();
    
        await Promise.all(cacheNames.map(async (cacheName) => {
          if (self.cacheName !== cacheName) {
            await caches.delete(cacheName);
          }
        }));
      })());
    });
    

    3. I update the cache name every time I have updated my assets:

    sw.js

    // this imported script has the newly generated cache name (self.cacheName)
    // and a list of all the files on my bucket I want to be cached (self.contentToCache),
    // and is automatically generated in Gitlab based on the tag version
    self.importScripts('cache.js');
    
    // the install event will be triggered if there's any update,
    // a new cache will be created (see 1.) and the old one deleted (see 2.)
    

    4. Handle Expires and Cache-Control response headers in cache

    I use these headers in the service worker's fetch event handler to catch whether it should request the resource via the network when a resource expired/should be refreshed.

    Basic example:

    // ...
    
    try {
      const cachedResponse = await caches.match(event.request);
    
      if (exists(cachedResponse)) {
        const expiredDate = new Date(cachedResponse.headers.get('Expires'));
    
        if (expiredDate.toString() !== 'Invalid Date' && new Date() <= expiredDate) {
          return cachedResponse.clone();
        }
      }
    
      // expired or not in cache, request via network...
    } catch (e) {
      // do something...
    }
    // ...
    

提交回复
热议问题