Handing notification flow to a service worker

Notification handling doesn't have to live entirely in the page context. This codelab walks through moving notification display logic into a service worker, so the worker receives messages from the page and is responsible for showing notifications. The instructions assume you're familiar with service workers and the basics of requesting notification permission; if you need a refresher, review the prior codelab on getting started with the Notifications API or the service worker overview.

Inspect the starting app

Open the live app in a new tab and open DevTools with Control+Shift+J (or Command+Option+J on Mac). In the Console tab, make sure the Info level is selected in the Levels dropdown next to the Filter box. You'll see a console message:

TODO: Implement getRegistration()

That message comes from a stub function you'll implement. Looking at public/index.js, you'll find four stubs: registerServiceWorker, getRegistration, unRegisterServiceWorker, and sendNotification. The file also contains a requestPermission function (which now updates the UI after resolving), an updateUI function that refreshes buttons and messages, and an initializePage function that performs feature detection and updates the UI. The script waits for the page to load before initializing.

The public/service-worker.js file isn't registered yet, but it contains starting code that prints a message to the console when the worker activates. You'll add notification handling code to it later.

Register and unregister the worker

First, implement the code that runs when the user clicks Register service worker. Replace registerServiceWorker in public/index.js with:

// Use the Service Worker API to register a service worker.
async function registerServiceWorker() {
  await navigator.serviceWorker.register('./service-worker.js')
  updateUI();
}

Note that registerServiceWorker uses the async function declaration, which lets you await the resolved value of a promise. In this case, the function awaits the outcome of the registration before updating the UI.

Next, you need a way to get a reference to the service worker registration object. Replace getRegistration with:

// Get the current service worker registration.
function getRegistration() {
  return navigator.serviceWorker.getRegistration();
}

This function uses the Service Worker API to retrieve the current registration, if one exists, making the reference easy to obtain.

To round out the registration functionality, replace unRegisterServiceWorker with:

// Unregister a service worker, then update the UI.
async function unRegisterServiceWorker() {
  // Get a reference to the service worker registration.
  let registration = await getRegistration();
  // Await the outcome of the unregistration attempt
  // so that the UI update is not superceded by a
  // returning Promise.
  await registration.unregister();
  updateUI();
}

After reloading the page, the Register service worker and Unregister service worker buttons should work.

Send notification data to the worker

Next, implement the code that runs when the user clicks Send a notification. This code creates a notification, verifies that a service worker is registered, and posts the notification to the worker. Replace sendNotification in public/index.js with:

// Create and send a test notification to the service worker.
async function sendNotification() {
  // Use a random number as part of the notification data
  // (so you can tell the notifications apart during testing!)
  let randy = Math.floor(Math.random() * 100);
  let notification = {
    title: 'Test ' + randy,
    options: { body: 'Test body ' + randy }
  };
  // Get a reference to the service worker registration.
  let registration = await getRegistration();
  // Check that the service worker registration exists.
  if (registration) {
    // Check that a service worker controller exists before
    // trying to access the postMessage method.
    if (navigator.serviceWorker.controller) {
      navigator.serviceWorker.controller.postMessage(notification);
    } else {
      console.log('No service worker controller found. Try a soft reload.');
    }
  }
}

Here's what happens in that code:

  • sendNotification is asynchronous, so you can await the service worker registration.
  • The worker's postMessage method transmits data from the page to the service worker.
  • The code checks navigator.serviceWorker.controller before calling postMessage. That property is null when there's no active service worker or when the page was force refreshed with Shift+Reload.

Handle messages inside the worker

Now add code to the service worker itself to handle posted messages and display notifications. Append the following to public/service-worker.js:

// Show notification when received
self.addEventListener('message', (event) => {
  let notification = event.data;
  self.registration.showNotification(
    notification.title,
    notification.options
  ).catch((error) => {
    console.log(error);
  });
});

Two points on this code:

  • self is a reference to the service worker itself.
  • The main app UI is still responsible for requesting notification permission. If permission wasn't granted, the promise returned by showNotification is rejected. The catch block handles that rejection gracefully so you don't end up with an uncaught promise error.

The pattern here moves notification display out of the page and into the worker, which keeps the page UI focused on permission and control while the worker owns the actual notification lifecycle.