Kermit
Kermit

Reputation: 3407

Workbox update cache on new version

I have implemented Workbox to generate my service worker using webpack. This works pretty well - I can confirm that revision is updated in the generated service worker when running yarn run generate-sw (package.json: "generate-sw": "workbox inject:manifest").

The problem is - I have noticed my clients are not updating the cache after a new release. Even days after updating the service worker my clients are still caching the old code and new code will only cache after several refreshes and/or unregister the service worker. For each release the const CACHE_DYNAMIC_NAME = 'dynamic-v1.1.0' is updated.

How can I ensure that clients updates the cache immediately after a new release?

serviceWorker-base.js

importScripts('workbox-sw.prod.v2.1.3.js')

const CACHE_DYNAMIC_NAME = 'dynamic-v1.1.0'
const workboxSW = new self.WorkboxSW()

// Cache then network for fonts
workboxSW.router.registerRoute(
  /.*(?:googleapis)\.com.*$/, 
  workboxSW.strategies.staleWhileRevalidate({
    cacheName: 'google-font',
    cacheExpiration: {
      maxEntries: 1, 
      maxAgeSeconds: 60 * 60 * 24 * 28
    }
  })
)

// Cache then network for css
workboxSW.router.registerRoute(
  '/dist/main.css',
  workboxSW.strategies.staleWhileRevalidate({
    cacheName: 'css'
  })
)

// Cache then network for avatars
workboxSW.router.registerRoute(
  '/img/avatars/:avatar-image', 
  workboxSW.strategies.staleWhileRevalidate({
    cacheName: 'images-avatars'
  })
)

// Cache then network for images
workboxSW.router.registerRoute(
  '/img/:image', 
  workboxSW.strategies.staleWhileRevalidate({
    cacheName: 'images'
  })
)

// Cache then network for icons
workboxSW.router.registerRoute(
  '/img/icons/:image', 
  workboxSW.strategies.staleWhileRevalidate({
    cacheName: 'images-icons'
  })
)

// Fallback page for html files
workboxSW.router.registerRoute(
  (routeData)=>{
    // routeData.url
    return (routeData.event.request.headers.get('accept').includes('text/html'))
  }, 
  (args) => {
    return caches.match(args.event.request)
    .then((response) => {
      if (response) {
        return response
      }else{
        return fetch(args.event.request)
        .then((res) => {
          return caches.open(CACHE_DYNAMIC_NAME)
          .then((cache) => {
            cache.put(args.event.request.url, res.clone())
            return res
          })
        })
        .catch((err) => {
          return caches.match('/offline.html')
          .then((res) => { return res })
        })
      }
    })
  }
)

workboxSW.precache([])

// Own vanilla service worker code
self.addEventListener('notificationclick', function (event){
  let notification = event.notification
  let action = event.action
  console.log(notification)

  if (action === 'confirm') {
    console.log('Confirm was chosen')
    notification.close()
  } else {
    const urlToOpen = new URL(notification.data.url, self.location.origin).href;

    const promiseChain = clients.matchAll({ type: 'window', includeUncontrolled: true })
    .then((windowClients) => {
      let matchingClient = null;
      let matchingUrl = false;
      for (let i=0; i < windowClients.length; i++){
        const windowClient = windowClients[i];

        if (windowClient.visibilityState === 'visible'){
          matchingClient = windowClient;
          matchingUrl = (windowClient.url === urlToOpen);
          break;
        }
      }

      if (matchingClient){
        if(!matchingUrl){ matchingClient.navigate(urlToOpen); }
        matchingClient.focus();
      } else {
        clients.openWindow(urlToOpen);
      }

      notification.close();
    });

    event.waitUntil(promiseChain);
  }
})

self.addEventListener('notificationclose', (event) => {
  // Great place to send back statistical data to figure out why user did not interact
  console.log('Notification was closed', event)
})

self.addEventListener('push', function (event){
  console.log('Push Notification received', event)

  // Default values
  const defaultData = {title: 'New!', content: 'Something new happened!', openUrl: '/'}
  const data = (event.data) ? JSON.parse(event.data.text()) : defaultData

  var options = {
    body: data.content,
    icon: '/images/icons/manifest-icon-512.png', 
    badge: '/images/icons/badge128.png', 
    data: {
      url: data.openUrl
    }
  }

  console.log('options', options)

  event.waitUntil(
    self.registration.showNotification(data.title, options)
  )
})

Should I delete the cache manually or should Workbox do that for me?

caches.keys().then(cacheNames => {
  cacheNames.forEach(cacheName => {
    caches.delete(cacheName);
  });
});

Kind regards /K

Upvotes: 6

Views: 9572

Answers (2)

Hector Diaz Contreras
Hector Diaz Contreras

Reputation: 34

One way to get WorkBox to update when you have the files locally, not on a CDN, is the following way:

  1. In your serviceworker.js file add an event listener so that WorkBox skips waiting when there is an update, my code looks like this:

     importScripts('Scripts/workbox/workbox-sw.js');
     if (workbox) {
    
         console.log('Workbox is loaded :)');
    
         // Add a message listener to the waiting service worker
         // instructing it to skip waiting on when updates are done. 
         addEventListener('message', (event) => {
             if (event.data && event.data.type === 'SKIP_WAITING') {
                 skipWaiting();
             }
         });
         // Since I am using Local Workbox Files Instead of CDN I need to set the modulePathPrefix as follows
         workbox.setConfig({ modulePathPrefix: 'Scripts/workbox/' });
    
         // other workbox settings ...
     }
    
  2. In your client side page add an event listener for loads if service worker is in the navigator. As a note I am doing this in MVC so I put my code in the _Layout.cshtml so that it can update from any page on my website.

     <script type="text/javascript">
         if ('serviceWorker' in navigator) {
             // Use the window load event to keep the page load performant
             window.addEventListener('load', () => {
                 navigator.serviceWorker
                     // register WorkBox, our ServiceWorker.
                     .register("<PATH_TO_YOUR_SERVICE_WORKER/serviceworker.js"), { scope: '/<SOME_SCOPE>/' })
                     .then(function (registration) {
                         /**
                          * Whether WorkBox cached files are being updated.
                          * @type {boolean}
                          * */
                         let updating;
    
                         // Function handler for the ServiceWorker updates.
                         registration.onupdatefound = () => {
                             const serviceWorker = registration.installing;
                             if (serviceWorker == null) { // service worker is not available return.
                                 return;
                             }
    
                             // Listen to the browser's service worker state changes
                             serviceWorker.onstatechange = () => {
                                 // IF ServiceWorker has been installed 
                                 // AND we have a controller, meaning that the old chached files got deleted and new files cached
                                 // AND ServiceWorkerRegistration is waiting
                                 // THEN let ServieWorker know that it can skip waiting. 
                                 if (serviceWorker.state === 'installed' && navigator.serviceWorker.controller && registration && registration.waiting) {
                                     updating = true;
                                     // In my "~/serviceworker.js" file there is an event listener that got added to listen to the post message.
                                     registration.waiting.postMessage({ type: 'SKIP_WAITING' });
                                 }
    
                                 // IF we had an update of the cache files and we are done activating the ServiceWorker service
                                 // THEN let the user know that we updated the files and we are reloading the website. 
                                 if (updating && serviceWorker.state === 'activated') {
                                     // I am using an alert as an example, in my code I use a custom dialog that has an overlay so that the user can't do anything besides clicking okay.
                                     alert('The cached files have been updated, the browser will re-load.');
                                     window.location.reload();
                                 }
                             };
                         };
    
                         console.log('ServiceWorker registration successful with scope: ', registration.scope);
                     }).catch(function (err) {
                         //registration failed :(
                         console.log('ServiceWorker registration failed: ', err);
                     });
             });
         } else {
             console.log('No service-worker on this browser');
         }
     </script>
    

Note: I used the browser's service worker to update my WorkBox cached files, also, I've only tested this in Chrome, I have not tried it in other browsers.

Upvotes: 0

Giorgi Lagidze
Giorgi Lagidze

Reputation: 831

I think your problem is related to the fact that when you make an update to the app and deploy, new service worker gets installed, but not activated. Which explains the behaviour why this is happening.

The reason for this is registerRoute function also registers fetch listeners , but those fetch listeners won't be called until new service worker kicks in as activated. Also, the answer to your question: No, you don't need to remove the cache by yourself. Workbox takes care of those.

Let me know more details. When you deploy new code, and if users close all the tabs of your website and open a new one after that, does it start working after 2 refreshes? If so , that's how it should be working. I will update my answer after you provide more details.

I'd suggest you read the following: https://redfin.engineering/how-to-fix-the-refresh-button-when-using-service-workers-a8e27af6df68 and follow the 3rd approach.

Upvotes: 3

Related Questions