Workbox update cache on new version
Asked Answered
S

2

6

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

Spatial answered 29/3, 2020 at 9:10 Comment(0)
F
3

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.

Flaxman answered 29/3, 2020 at 14:11 Comment(8)
Hi Giorgi! Thank you for the prompt reply. I have tested and on desktop two refreshes updates the app. The PWA on Android is harder to fix, I have a refresh button in my app that I have clicked twice, but the app is still showing old icons and old behaviour. Kind regards /KSpatial
I simply reload the url using const refreshPage = () => { window.parent.location = window.parent.location.href } /KSpatial
refreshing won't activate new service worker. you have to use skipWaiting for that. do you use it?Flaxman
No, not at the present. I will try to implement that! Do you have any tips on how to implement "skipWaiting"? I found this article: redfin.engineering/… Many thanks! /KSpatial
This is exactly the article I followed in the past. I will update my answer with this link and make sure to mark the answer as correct if you feel so. Follow #3 approach. Download his code from github and understand what it does . then use it in your app.Flaxman
I am struggling to implement this, does it only work for PWA or should the implementation also work on desktop?Spatial
It should work on Desktop too. I know it's not easy. It could be many things. Write a new question and copy the link here and I will try to answer it there .Flaxman
Thanks Giorgi! I have posted a new question here: #61040926Spatial
S
0

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.

Studner answered 23/7, 2020 at 16:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.