ServiceWorker: Lifecycle, Update, and Notification
Andrew
Posted on September 30, 2020
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
- Event: install
- Event: activate
- Event: fetch
- Event: message
- Event: updatefound & statechange
- Web Push Notification
- Event: push
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;
}
});
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);
})
);
});
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);
})
);
});
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;
}
});
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" });
}
- receive message
self.addEventListener("message", (event) => {
if (event.data.action === "skipWaiting") {
// skip waiting to apply the new version of service worker
self.skipWaiting();
}
});
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
}
});
}
});
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",
},
};
*/
});
});
}
}
});
}
- 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);
});
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');
}
}
});
Conclusion
That's it, I hope this article can help you get familiar with the service worker.
Reference
Posted on September 30, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.