DEV Community

Sébastien NOBILI
Sébastien NOBILI

Posted on • Originally published at techreads.pipoprods.org

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 talk to each other.

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();
});
Enter fullscreen mode Exit fullscreen mode

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`;

/*-----8<-----8<-----8<-----8<-----*/
// Communication channel with clients
const channel = new BroadcastChannel('sw-messages');
/*-----8<-----8<-----8<-----8<-----*/
Enter fullscreen mode Exit fullscreen mode

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 {
/*-----8<-----8<-----8<-----8<-----*/
     channel.postMessage({ loading: true });
/*-----8<-----8<-----8<-----8<-----*/
     try {
       response = await fetch(resource);
       await put_into_cache(resource, response);
/*-----8<-----8<-----8<-----8<-----*/
       channel.postMessage({ loading: false });
       channel.postMessage({ offline: false });
/*-----8<-----8<-----8<-----8<-----*/
       return response;
     } catch (error) {
       // TODO: check if error is because we're offline
       response = await get_from_cache(resource);
/*-----8<-----8<-----8<-----8<-----*/
       channel.postMessage({ loading: false });
       channel.postMessage({ offline: true });
/*-----8<-----8<-----8<-----8<-----*/
       if (response) {
         // resource was found in data cache
         return response;
       } else {
         return new Response('offline', {
           status: 408,
           headers: { 'Content-Type': 'text/plain' },
         });
       }
     }
   }
 }
Enter fullscreen mode Exit fullscreen mode

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>

<!-----8<-----8<-----8<-----8<----->
     <div id="offline">
       You're currently offline. The data below may differ from the current
       online version.
     </div>

     <div id="loading">loading...</div>
<!-----8<-----8<-----8<-----8<----->

     <pre id="result"></pre>
   </body>
Enter fullscreen mode Exit fullscreen mode

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;
      }

/*-----8<-----8<-----8<-----8<-----*/
      #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;
      }

/*-----8<-----8<-----8<-----8<-----*/
      pre {
        display: flex;
        margin-left: auto;
        margin-right: auto;
      }
Enter fullscreen mode Exit fullscreen mode

Attach to the communcation channel:

document.addEventListener('DOMContentLoaded', () => {
  fetch_data(1);
  if ('serviceWorker' in navigator) {
/*-----8<-----8<-----8<-----8<-----*/
    const channel = new BroadcastChannel('sw-messages');
    channel.addEventListener('message', (event) => handle_message(event.data));
/*-----8<-----8<-----8<-----8<-----*/
    navigator.serviceWorker.register('sw.js');
  }
});
Enter fullscreen mode Exit fullscreen mode

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';
  }
}
Enter fullscreen mode Exit fullscreen mode

The UI will display a warning when offline:

Offline indicator

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:

  • pass data from a client to another (multi-window app)
  • make navigation more dynamic returning cached API data first, then updating it in the background

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

Top comments (0)