ServiceWorker: Lifecycle, Update, and Notification

oahehc

Andrew

Posted on September 30, 2020

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

💖 💪 🙅 🚩
oahehc
Andrew

Posted on September 30, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

ServiceWorker: Lifecycle, Update, and Notification