How to properly "update" a PWA?

Hi,

We are working in a F7 PWA project. Everytime we have a new realease, we follow this steps:

  1. npm run build-prod
  2. rsync the ‘www’ folder to the hosting / web server.

This work ok but requires to clear browser cache in order to see the last version. Is there any way to avoid that?

Thanks in advance!

2 Likes

The browser caches the application. To avoid this, you can tell the browser not to cache by making some changes in service-worker.js.

https://developers.google.com/web/ilt/pwa/introduction-to-service-worker

Thanks @uslualper!

Checking the ‘service worker’ documentation I understood that this is not a topic directly related to F7.

Anyway, I found a solution that can be helpful to others in the community.

export function forceServiceWorkerUpdate() {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.getRegistrations().then(function (registrations) {
      for (let registration of registrations) {
        // registration.update();  does not work!
        registration.unregister();
      }
      window.location.reload(true);
    });
  }
}
2 Likes

Thanks for the code. registration.unregister() combined with window.location.reload(true) works perfect!

Hi goodbytes

I’m just finished my first PWA with F7.

About your function, where do you call it?

Regards

Here’s how I do it in a flexible way.

Let’s take a look at the service worker.
Given this function (in the service worker):

function cacheRequestResponse(request,response){
  // Check if we received a valid response
  if(!response || response.status !== 200 || response.type !== 'basic') {
    return response;
  }

  //IMPORTANT: Ignore the "/watcher.js" script.
  //This should be the only file in your application that does not get cached locally.
  //The reason being is that this script should fulfill the function of an "updater" of sorts,
  //which will notify the client when there's an update by uncaching specific files (implemented manually),
  //and in order to do that this script must always be served directly by the server.
  if(request.url.endsWith(".updated.js")) return response;

  // IMPORTANT: Clone the response. A response is a stream
  // and because we want the browser to consume the response
  // as well as the cache consuming the response, we need
  // to clone it so we have two streams.
  let responseToCache = response.clone();

  caches.open(CACHE_NAME)
    .then(function(cache) {
      cache.put(request, responseToCache);
    });
}

This function will return files from the local cache unless their name ends with “.updated.js”.
If their name ends with “.updated.js” return a network response, aka the browser will ask the actual server for the contents of the file.

Here’s my listener:

self.addEventListener('fetch', function(event) {
  console.log("fetching",event.request.url);
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        // Cache hit - return response
        if (response) {
          return response;
        }

        return fetch(event.request).then(
          function(response) {
            cacheRequestResponse(event.request,response);
            return response;
          }
        );
      })
    );

At this point all you have to do is create a file “/some-file.updated.js” and include it in your index.html:

<html>
	...
	<body>
		<script async src='/some-file.updated.js'></script>
	</body>
</html>

At this point all you have to do is make some kind of contract between your entry file and your .updated.js file.

For example I’m using svelte, so I make a global store that stores my local “lazy” version of the application, and at the same time my .updated.js file contains also a “live” version number.

Whenever I make a change in my source code, I make sure these 2 numbers are hard coded and are the same (in my source code).

Whenever I update my .updated.js file, my clients will fetch that file updated (cause, remember, my service worker always tries to fetch that one from the server) their .updated.js file will have a function that checks if the 2 versions match.

When I push an update, their svelte store version won’t match and that will trigger my .updated.js file to, in my case, remove all the local cache and redownload the files from the server using this piece of code:

for(let i = 0; i < data.urls.length; i++){
	const response = await fetch(data.urls[i],{
		method:"GET",
		headers:{
			pragma: "no-cache",
			"cache-control": "no-cache"
		}
	});
}

Where data.urls contain all the urls of the assets your website uses (js,css, images, audio files ecc…).

If you’re wondering how I’m getting all these urls, no, I’m not hard coding them, I let my service worker map them when they get fetched:

let cacheNames = await caches.keys(); //this code lies on my service worker

I hope you got an idea on how to do it, this works pretty well for me and I’m currently using it in production, it gives me pretty good flexibility.

If the explenation above is not enough, you can check my template: https://github.com/tncrazvan/svelte-framework7-template

This function specifically


recieves a callback that gets called and gets passed the actual live version and you can make your own store to save the “lazy” version number so to speak.


Hope this is helpful!

2 Likes

I have this in my app.vue mounted hook component:

      // When a new service worker is installed, we notify the user so he can restart the app
      f7.on('serviceWorkerRegisterSuccess', (registration) => {
        console.log('serviceWorkerRegisterSuccess');
        const reg = registration;
        reg.onupdatefound = () => {
          console.log('serviceWorker onupdatefound');
          const installingWorker = reg.installing;
          installingWorker.onstatechange = () => {
            console.log('serviceWorker installingWorker onstatechange');
            if (installingWorker.state === 'installed') {
              console.log('serviceWorker installed');
              if (navigator.serviceWorker.controller) {
                console.log('serviceWorker navigator.serviceWorker.controller');
                // We inform the user a new update is available
                this.notifyUserOfUpdate(() => {
                  window.location.reload();
                });
              }
            }
          };
        };
      });

When it detects a new version, it displays a notification. If the user click the notification, the page automatically reloads. Otherwise, the new service worker will be installed the next time the user load the app.

    notifyUserOfUpdate(restart) {
      this.$f7.notification.create({
        icon: '<img src="static/icons/favicon.png">',
        title: 'MyApp',
        subtitle: this.$t('newUpdate-title'),
        text: this.$t('newUpdate-text'),
        closeButton: true,
        on: {
          click() {
            restart();
          },
        },
      }).open();
    },

For people like me who use F7 core and need a mandatory restart:

Into workbox-config.js add:

skipWaiting: true

Into app.js add:

app.on('serviceWorkerRegisterSuccess', (registration) => {
    const reg = registration;
    //add event in routeChange because page doesn't change so i manually update service
    app.on('routeChange', () => {
        reg.update();
    });
    reg.onupdatefound = () => {
        const installingWorker = reg.installing;
        installingWorker.onstatechange = () => {
            if (installingWorker.state === 'installed') {
                app.dialog.create({
                    text: 'To be sure of the functioning of the application, press the update button.',
                    title: `Update ${app.name} detected!`,
                    buttons: [
                        {
                            text: 'Update',
                            onClick: (dialog, e) => {
                                window.location.reload();
                            }
                        },
                        {
                            text: 'Close',
                            onClick: (dialog, e) => {
                                dialog.close();
                            }
                        }
                    ],
                    verticalButtons: true
                }).open();                
            }
        };
    };
});

Another piece of advice that I give you to prevent the dialogue from activating (on firefox/edge) even on the first start, just put an init event on the application instance with a “first start” variable like:

var app = new Framework7({
  //other code
  on: {
        init: function (page) {
           var f7 = this;
           f7.firstStart = true;
        }

  }
)};

And change this line into reg.onupdatefound event:

if (installingWorker.state === 'installed'  && !app.firstTime) {
 // other code.
} else {
  app.firstTime = false;
}
1 Like

Just a small edit:

if ((installingWorker.state === 'installed' || installingWorker.state === 'activated') && !app.firstTime) {