DEV Community

Cover image for ServiceWorker: Lifecycle, Update, and Notification
Andrew
Andrew

Posted on

3

ServiceWorker: Lifecycle, Update, and Notification

If you have tried CRA (create react app), have you ever wonder what does this file - `/src/serviceWorker.js` do? In this article, I will demonstrate what we can do by implementing the service worker into our application.

Before we start, the service worker might be buggy once we didn't handle it properly, therefore, I highly recommend you check this article to know a few important knowledge beforehand - Offline-First Considerations

Agenda

Register Service Worker

At first, we need to register service worker.

  navigator.serviceWorker
    .register("/sw.js")
    .then((reg) => {
      // no controller exist, page wasn't loaded via a service worker
      if (!navigator.serviceWorker.controller) {
        return;
      }

      if (reg.waiting) {
        // If we have a new version of the service worker is waiting,
        // we can display the message to the user and allow them
        // to trigger updates manually.
        // Otherwise, the browser will replace the service worker
        // when the user closes or navigate away from all tabs using
        // the current service worker.
        return;
      }
      if (reg.installing) {
        // If we have a new service worker is installing, we can
        // tracking the status and display the message once the
        // installation is finished.
        return;
      }
    });
Enter fullscreen mode Exit fullscreen mode

Event: install

The install event is the first event a service worker gets, and it only happens once.

We can cache the pages here.

const urlsToCache = ["/faq", "/contact"];
self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(urlsToCache);
    })
  );
});
Enter fullscreen mode Exit fullscreen mode

Event: activate

Once your service worker is ready to control clients, we'll get an activate event.

It's common to delete the old caches here.

self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      const promiseArr = cacheNames.map((item) => {
        if (item !== CACHE_NAME) {
          return caches.delete(item);
        }
      });
      return Promise.all(promiseArr);
    })
  );
});
Enter fullscreen mode Exit fullscreen mode

Event: fetch

We can intercept the request and custom the response in the fetch event.

self.addEventListener("fetch", (event) => {
  // hijacking path and return a mock HTML content
  if (event.request.url.includes("/faq")) {
    event.respondWith(
      new Response("<div>Mock FAQ Page</div>", {
        headers: { "Content-Type": "text/html" },
      })
    );
  }
  // hijacking API request and return mock response in JSON format
  if (event.request.url.includes("/api/users")) {
    const data = [
      {
        id: "0001",
        name: "andrew",
      },
    ];
    const blob = new Blob(
      [JSON.stringify(data, null, 2)],
      { type: "application/json" }
    );
    const init = { status: 200, statusText: "default mock response" };
    const defaultResponse = new Response(blob, init);
    event.respondWith(defaultResponse);
  }

  // Stale-while-revalidate:
  // return the cached version if it exists. At the same time,
  // send a request to get the latest version and update the cache
  const requestUrl = new URL(event.request.url);
  if (requestUrl.pathname.startsWith("/avatars/")) {
    const response = caches.open(CACHE_NAME).then((cache) => {
      return cache.match(event.request).then((response) => {
        const networkFetch = fetch(event.request)
          .then((networkResponse) => {
            cache.put(event.request, networkResponse.clone());
            return networkResponse;
          });

        return response || networkFetch;
      });
    });

    event.respondWith(response);
    return;
  }
});
Enter fullscreen mode Exit fullscreen mode

Event: message

We can use postMessage to communicate with the service worker.
Here, we bind a click event to send a postMessage to the service worker.
In the service worker, we can listen to the message event to received the postMessage.

  • send message to service worker
function handleClickEven() {
  worker.postMessage({ action: "skipWaiting" });
}
Enter fullscreen mode Exit fullscreen mode
  • receive message
self.addEventListener("message", (event) => {
  if (event.data.action === "skipWaiting") {
    // skip waiting to apply the new version of service worker
    self.skipWaiting(); 
  }
});
Enter fullscreen mode Exit fullscreen mode

Event: updatefound & statechange

We can listen to the updatefound event to see if we have a new service worker.
If there is a service worker is installing, we listen to the statechange event,
once the install is finished, we can display a message to notify our users.

self.addEventListener("updatefound", () => {
  if (reg.installing) {
    reg.installing.addEventListener("statechange", () => {
      if (worker.state == "installed") {
        // display a message to tell our users that
        // there's a new service worker is installed
      }
    });
  }
});
Enter fullscreen mode Exit fullscreen mode

Web Push Notification

We can use a service worker to handle the notification.
Here, we ask permission to display the notification, if the user agrees,
then we can get the subscription information.

  • Get permission & subscription
if (Notification && Notification.permission === "default") {
  Notification.requestPermission().then((result) => {
    if (result === "denied") {
      return;
    }

    if (result === "granted") {
      if (navigator && navigator.serviceWorker) {
        navigator.serviceWorker.ready.then((reg) => {
          reg.pushManager
            .getSubscription()
            .then((subscription: any) => {
              if (!subscription) {
                // we need to encrypt the data for web push notification,
                // I use web-push to generate the public and private key.
                // You can check their documentation for more detail.
                // https://github.com/web-push-libs/web-push
                const vapidPublicKey = "xxxxx";
                const applicationServerKey = 
                  urlBase64ToUint8Array(vapidPublicKey);
                return reg.pushManager.subscribe({
                  userVisibleOnly: true,
                  applicationServerKey,
                });
              }

              return subscription;
            })
            .then((sub) => {
              // Get the subscription information here, we need to
              // create an API and save them into our database
              /*
                {
                  endpoint:
                    "https://fcm.googleapis.com/fcm/send/xxx",
                  keys: {
                    auth: "xxx",
                    p256dh: "xxx",
                  },
                };
              */
            });
        });
      }
    }
  });
}
Enter fullscreen mode Exit fullscreen mode
  • send notification
const webpush = require('web-push');

webpush.setVapidDetails("mailto:oahehc@gmail.com", "my_private_key");
const pushConfig = {
  endpoint: sub.endpoint,
  keys: {
    auth: sub.keys.auth,
    p256dh: sub.keys.p256dh,
  },
};
webpush
  .sendNotification(
    pushConfig,
    JSON.stringify({ title: "Test Title", content: "Test Content" })
  )
  .catch((err) => {
    console.log(err);
  });
Enter fullscreen mode Exit fullscreen mode

Event: push

We can receive the web push message by listening to the push event.

self.addEventListener("push", (event) => {
  if (event.data) {
    try {
      data = JSON.parse(event.data.text());
      event.waitUntil(
        self.registration.showNotification(data.title, {
          body: data.content,
          icon: "/icon-192.png",
          badge: "/badge-192.png",
        })
      );
    } catch (e) {
      console.error('push event data parse fail');
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

That's it, I hope this article can help you get familiar with the service worker.


Reference

Heroku

This site is built on Heroku

Join the ranks of developers at Salesforce, Airbase, DEV, and more who deploy their mission critical applications on Heroku. Sign up today and launch your first app!

Get Started

Top comments (0)