Updating your PWA in production

Have you heard the joke that if you installed ServiceWorker, it's time to change the domain? Now I will tell you what its meaning is and what to do if you nevertheless decide that you need a PWA.





In the instructions for the type of this or this ServiceWorker and working with it, almost no attention is paid. And I'm sure articles like this are the first thing you read before using. But at the moment when, after such articles, your freshly baked PWA finally appeared on production and users had the opportunity to add another icon to their desktop, know that you have passed the point of no return.





With your permission, I will not dwell on the description of Service Worker (hereinafter SW) and how it works. Habré already has a good article about this . It doesn't even matter which SW you have specifically. Maybe you are using create-react-app , which means that the Workbox library is responsible for SW for you . Perhaps you implemented SW yourself, with some tricky caching strategy. The stack is not really important. The same CRA documentation says that all you need is to change one line and get all the delights of app-like behavior. You have written .register()



and expect the result. And you will get it.





The next time a disgruntled customer asks you to change the color of this orange button or finally solve that flying focus bug, you will find yourself in an amazing situation. There is a hotfix in the repository, the container is assembled and nginx distributes the latest version for sure, but the client is still unhappy for some reason. Oh yes, we are now PWA.





- Please refresh the page. How does it not help? What if CTRL + R?





So what do you do when the frantic page refresh doesn't help and the customer still sees the mocking orange button?





It's important to remember that SW tries to behave like a desktop application.





Let's remember how the desktop application updates itself. It downloads the fresh installer, removes the old version, and reinstalls it. Only then does the user receive a new version of the application.





SW.





SW : installing, waiting active. Active - , SW. installing waiting SW active. installing SW , . waiting , SW ( ). .





SW, , - . SW , . . , . , .





, , "". , SW : , , , . , SW . , .





SW installing, waiting active. - . , .





, , .





№1: SW

( ) - SW. SW skipWaiting()



, . SW . "" .

: , . , skipWaiting()



, , .





№2: SW

, . navigator.serviceWorker



controllerchange



, SW . installing.

skipWaiting()



, . :





navigator.serviceWorker.addEventListener('controllerchange',  ()  => window.location.reload());
      
      



SW , .

, , . , , . .





№3:

, , SW , - - .





controllerchange



, , , .

, SW, ServiceWorkerRegistration



. .register()



, . API . , update()



, SW . , .





(active) SW navigator.serviceWorker.controller



active . (waiting) (installing) SW.





SW postMessage()



, iframe , API. SW . SW.





addEventListener('message', ev => {  
  if (ev.data === 'skipWaiting') return skipWaiting();
});
      
      



Workbox CRA, .





Next, we need to track the appearance of the waiting SW. In my opinion, it is better not to react every time to SW with the installing status, as it is written in some manuals, but to wait until the SW registration object returns true in the field waiting



. This will slow down the update, but will not trigger your modal when the SW is installed for the first time.





After we have waited for the pending SW, we call a modal window in which the user can confirm the update. On confirmation, we call skipWaiting()



and forcibly reload the page as described above. If a failure occurs, the update will be postponed. The code in my case will look like this:





//   
const askUserToUpdate = reg => {
  return Modal.confirm({
    onOk: async () => {
      //    
      navigator.serviceWorker.addEventListener('controllerchange', () => {
        window.location.reload();
      });

      //   
      if (reg && reg.waiting) {
        reg.waiting.postMessage({ type: 'SKIP_WAITING' });
      }
    },

    onCancel: () => {
      Modal.destroyAll();
    },
    icon: null,
    title: ' ! 
      
      










All Articles