Hire a Service Worker for Your Website (ii)

Image for post
Image for post
Photo by Alaina McLearnon on Unsplash

In the last post we were discussing about the basics about Service Worker. In this post, we are walk through an example of how a Worker get initiated.


if ("serviceWorker" in navigator) {
//do something like:
initServiceWorker().catch(console.error);
}
var isOnline = "onLine" in navigator ? navigator.onLine : true;
var isLoggedIn = /isLoggedIn=1/.test(document.cookie.toString() || "");
// let the page monitor if we are logged in since worker can't do it
var swRegistration;
var svcworker;

async function initServiceWorker() {
swRegistration =
await navigator.serviceWorker.register(
"/sw.js",{updateViaCache: "none",});

svcworker =
swRegistration.installing ||
swRegistration.waiting ||
swRegistration.active;
sendStatusUpdate(svcworker);
// listen for new service worker to take over
navigator.serviceWorker.addEventListener(
"controllerchange",
async function onController(){
svcworker = navigator.serviceWorker.controller;
sendStatusUpdate(svcworker);
});

navigator.serviceWorker.addEventListener(
"message",onSWMessage,false);
}

function onSWMessage(evt) {
var { data } = evt;
if (data.statusUpdateRequest) {
console.log(
"Status update requested from service worker, responding...");
sendStatusUpdate(evt.ports && evt.ports[0]);
} else if (
data == "force-logout"
) {
document.cookie = "isLoggedIn=";
isLoggedIn = false;
sendStatusUpdate();
}
}

function sendStatusUpdate(target) {
sendSWMessage(
{ statusUpdate: { isOnline, isLoggedIn } },target);
}

function sendSWMessage(msg,target) {
if (target) {
target.postMessage(msg);
}
else if (svcworker) {
svcworker.postMessage(msg);
}
else if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage(msg);
}
}
}

It’s pretty long, but don’t worry, let’s go through it step by step.

First we are init a few variables as they are essential to our later steps. Note that the isLoggedIn, this is because we have to let the page monitor if we are logged in since a Service Worker can’t access the cookie. And we will pass this status back to the Worker through the page client.

Next we register the worker:

await navigator.serviceWorker.register(
"/sw.js",{updateViaCache: "none",});

Note the worker is just a JavaScript file residing inside our app (note this is the file’s URL relative to the origin, not the JS file that references it.)

The updateViaCache is to manage the browsers HTTP cache. When set to 'imports', the HTTP cache will never be consulted when checking for updates to the /service-worker.js script, but will be consulted when fetching any imported scripts (path/to/import.js, in our example). This is the default. When set to 'all', the HTTP cache will be consulted when making requests for both the top-level /service-worker.js script, as well as any scripts imported inside of the service worker, like path/to/import.js. When set to 'none', the HTTP cache will not be consulted when making requests for either the top-level /service-worker.js or for any imported scripts, such as the hypothetical path/to/import.js.

Next we wait for a status update. The state is read-only property of the ServiceWorker interface returns a string representing the current state of the service worker. It can be one of the following values: installing, installed, activating, activated, or redundant.

Next, we are waiting for a controller change update. The controller read-only property of the ServiceWorkerContainer interface returns a ServiceWorker object if its state is activated. This property returns null if the request is a force refresh or if there is no active worker. The concontrollerchange property of the ServiceWorkerContainer interface is an event handler fired whenever a controllerchange event occurs — when the document's associated ServiceWorkerRegistration acquires a new active worker.

The postMessage() method of the Client interface allows a service worker to send a message to a client (a Window, Worker, or SharedWorker). The message is received in the "message" event on navigator.serviceWorker. On receiving that message:

navigator.serviceWorker.addEventListener('message', event => {
console.log(event.data.msg, event.data.url);
});

MessageEvent.ports Read only. An array of MessagePort objects representing the ports associated with the channel the message is being sent through (where appropriate, e.g. in channel messaging or when sending a message to a shared worker).

onconnect = function(e) {
var port = e.ports[0];

port.addEventListener('message', function(e) {
var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
port.postMessage(workerResult);
});

port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter.
}

The next step we will do some corresponding handling in the worker file.

"use strict";const version = 1;
var isOnline = true;
var isLoggedIn = false;
var cacheName = `myWeb-${version}`;
self.addEventListener("install", onInstall);
self.addEventListener("activate", onActivate);
self.addEventListener("message", onMessage);
main().catch(console.error);async function main() {
await sendMessage({ requestStatusUpdate: true });
console.log(`service worker ${version} is starting ...`);
}
async function onInstall(e) {
console.log(`service worker ${version} is installed ...`);
self.skipWaiting();
}
async function sendMessage(msg) {
//first get all clients
var allClients = await clients.matchAll({ includeUncontrolled: true });
// give them promises
return Promise.all(
allClients.map(function sendTo(client) {
var chan = new MessageChannel();
// Listen for messages on port1
chan.port1.onmessage = onMessage;
// Transfer port2 to the client
return client.postMessage(msg, [chan.port2]);
})
);
}
// Handle messages received on port1
function onMessage({ data }) {
if ("statusUpdate" in data) {
({ isOnline, isLoggedIn } = data.statusUpdate);
console.log(
`Service Worker (v${version}) status update... isOnline:${isOnline}, isLoggedIn:${isLoggedIn}`
);
}
}
async function onActivate(e) {
// tell not to shut down until handleActivation is finished
e.waitUntil(handleActivation());
}
async function handleActivation() {
// get the new service worker come into control
// (in case old pages still under control of old workers)
await clients.claim();
console.log(`service worker ${version} is activated ...`);
}

Here you can see we have all the events corresponding to the client file: install, activate, message.

The important part here worth mentioning is the MessagePort interface of the Channel Messaging API representing one of the two ports of a MessageChannel, allowing messages to be sent from one port and listening out for them arriving at the other.

We first create a new channel being created using the MessageChannel() constructor.

We then listen for messages on port1. The port1 read-only property of the MessageChannel interface returns the first port of the message channel — the port attached to the context that originated the channel.

We then transfer port2 to the client. The port2 read-only property of the MessageChannel interface returns the second port of the message channel — the port attached to the context at the other end of the channel, which the message is initially sent to.

So that’s so much of it.

Normally during the install step, you’ll want to cache some static assets for the Worker. So we will talk about this in next blog.

Happy Reading!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store