ServiceWorker: Lifecycle, Update, and Notification
Tags: serviceworker, webpush, notification
Published: 2020-09-30 03:32:38

<blockquote>
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.
</blockquote>

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](https://create-react-app.dev/docs/making-a-progressive-web-app/#offline-first-considerations)

## Agenda
  * [Register Service Worker](#agenda-1)
  * [Event: install](#agenda-2)
  * [Event: activate](#agenda-3)
  * [Event: fetch](#agenda-4)
  * [Event: message](#agenda-5)
  * [Event: updatefound & statechange](#agenda-6)
  * [Web Push Notification](#agenda-7)
  * [Event: push](#agenda-8)

### Register Service Worker <a name="agenda-1"></a>
At first, we need to register service worker.

```js
  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;
      }
    });
```

### Event: install <a name="agenda-2"></a>
The `install` event is the first event a service worker gets, and it only happens once.  
We can cache the pages here.

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

### Event: activate <a name="agenda-3"></a>
Once your service worker is ready to control clients, we'll get an `activate` event.  
It's common to delete the old caches here.

```js
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);
    })
  );
});
```

### Event: fetch <a name="agenda-4"></a>
We can intercept the request and custom the response in the `fetch` event.

```js
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;
  }
});
```

### Event: message <a name="agenda-5"></a>
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
```js
function handleClickEven() {
  worker.postMessage({ action: "skipWaiting" });
}
```
- receive message
```js
self.addEventListener("message", (event) => {
  if (event.data.action === "skipWaiting") {
    // skip waiting to apply the new version of service worker
    self.skipWaiting(); 
  }
});
```

### Event: updatefound & statechange <a name="agenda-6"></a>
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.

```js
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
      }
    });
  }
});
```

### Web Push Notification <a name="agenda-7"></a>
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
```js
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",
                  },
                };
              */
            });
        });
      }
    }
  });
}
```

- send notification
```js
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);
  });
```

### Event: push <a name="agenda-8"></a>
We can receive the web push message by listening to the `push` event.

```js
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');
    }
  }
});
```

---

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

---

## Reference
  * <span>Photo by <a href="https://unsplash.com/@anniespratt?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Annie Spratt</a> on <a href="https://unsplash.com/s/photos/offline?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></span>
  * [Offline Web Applications](https://classroom.udacity.com/courses/ud899)
  * [The Service Worker Event](https://developers.google.com/web/fundamentals/primers/service-workers/Event)
  * [Making a Progressive Web App](https://create-react-app.dev/docs/making-a-progressive-web-app/#offline-first-considerations)
  * [The Service Worker Lifecycle](https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle)
  * [web-push](https://github.com/web-push-libs/web-push)