Hire a Service Worker for Your Website (ii)

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.
Step 1: Check availability
if ("serviceWorker" in navigator) {
//do something like:
initServiceWorker().catch(console.error);
}
Step 2: Registration
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.
}
Step 3. Handle in Worker
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!