Mastodon Skip to main content
techreads

PWA: Communicating Between the Frontend & the Service Worker

In a previous article we saw how to create a basic PWA that works offline. This PWA had a Service Worker that was transparently issuing requests for the frontend, returning cached data if network was unreachable.

This article will go deeper into the PWA world and we'll make both the frontend and the Service Worker discuss together.

The code examples in this article are based on the code written for previous article. The whole code is available here: https://code.pipoprods.org/web/simple-pwa.

Making the Service Worker instantly control the page #

You probably noticed that once installed, the Service Worker was not working for the page that triggered install. That's an expected behavior.

Fortunately, the API gives us a method to set the Service Worker active for all the clients it is attached to.

Let's make our Service Worker claim its clients:

/**
 * Then take immediate control on the page that triggered the installation
 */
self.addEventListener('activate', () => {
  self.clients.claim();
});

Detecting online/offline & loading state #

Next step, we'll implement a message channel between the Service Worker and its clients. This channel will be used to broadcast messages to update the UI according to network status.

On the Service Worker side, create a communication channel:

 const appCacheName = 'simple-pwa';
 const appContentToCache = [
   '/',
   'index.html',
   'app.js',
   'favicon.ico',
   'manifest.webmanifest',
   'icons/icon-512.png',
 ];
 const dataCacheName = `${appCacheName}-data`;

+// Communication channel with clients
+const channel = new BroadcastChannel('sw-messages');

Then write messages when something needs to be reported to the frontend:

 async function fetch_resource(resource) {
   response = await get_from_cache(resource, appCacheName);
   if (response) {
     return response;
   } else {
+    channel.postMessage({ loading: true });
     try {
       response = await fetch(resource);
       await put_into_cache(resource, response);
+      channel.postMessage({ loading: false });
+      channel.postMessage({ offline: false });
       return response;
     } catch (error) {
       // TODO: check if error is because we're offline
       response = await get_from_cache(resource);
+      channel.postMessage({ loading: false });
+      channel.postMessage({ offline: true });
       if (response) {
         // resource was found in data cache
         return response;
       } else {
         return new Response('offline', {
           status: 408,
           headers: { 'Content-Type': 'text/plain' },
         });
       }
     }
   }
 }

On the frontend side, let's add elements to our HTML:

   <body>
     <h1>Simple PWA</h1>

     <div class="controls">
       <button onClick="previous()">&lt;</button>
       <span id="api_id"></span>
       <button onClick="next()">&gt;</button>
     </div>

+    <div id="offline">
+      You're currently offline. The data below may differ from the current
+      online version.
+    </div>
+
+    <div id="loading">loading...</div>
+
     <pre id="result"></pre>
   </body>

Style them:

       html,
       body {
         height: 100%;
         width: 80%;
         margin: 0;
         display: flex;
         flex-direction: column;
         margin-left: auto;
         margin-right: auto;
         font-family: sans-serif;
       }

       h1,
       div.controls {
         text-align: center;
       }

       #api_id {
         font-family: monospace;
         text-align: center;
         display: inline-block;
         width: 5em;
       }

+      #offline {
+        border: 1px solid red;
+        background-color: rgba(255, 0, 0, 0.1);
+        padding: 5px;
+        text-align: center;
+        color: red;
+        margin-top: 10px;
+        margin-bottom: 10px;
+        visibility: hidden;
+      }
+
+      #loading {
+        text-align: center;
+        visibility: hidden;
+      }
+
       pre {
         display: flex;
         margin-left: auto;
         margin-right: auto;
       }

Attach to the communcation channel:

 document.addEventListener('DOMContentLoaded', () => {
   fetch_data(1);
   if ('serviceWorker' in navigator) {
+    const channel = new BroadcastChannel('sw-messages');
+    channel.addEventListener('message', (event) => handle_message(event.data));
     navigator.serviceWorker.register('sw.js');
   }
 });

Finally, handle the incoming messages:

function handle_message(message) {
  if (message.offline !== undefined) {
    document.getElementById('offline').style.visibility = message.offline
      ? 'visible'
      : 'hidden';
  }

  if (message.loading !== undefined) {
    document.getElementById('loading').style.visibility = message.loading
      ? 'visible'
      : 'hidden';
    document.getElementById('result').style.visibility = message.loading
      ? 'hidden'
      : 'visible';
  }
}

The UI will display a warning when offline:

Offline warning

And a loading indicator:

Loading indicator

Final thoughts #

In this article, we implemented a simple broadcast communication between the Service Worker and its clients. This could be used for more complex things:

The source code is here: https://code.pipoprods.org/web/simple-pwa

Let's discuss about this on Mastodon!