Anatomy of Service Worker Communication

Let your App communicate with its Service Worker

I have a SPA that works as a PWA, which means that in the background a service worker makes sure that the required files for the offline mode end up in the cache.

From time to time I also update the Service Worker, which defines which files it should keep offline and which not. Unfortunately, the app itself didn’t get any of this because there was no communication channel for them to talk.

If you research this topic on the web, you have to dig through many architecture pages and documentations that have one thing in common: sometimes they just don’t get to the point. So here are my 50 cents on the subject and my sample implementation.

Preliminary Thoughts

In most examples I’ve read, the authors talk about code for the app itself and code for the Service Worker, but that falls short to me, because in my opinion there are THREE parts:

  1. The App
  2. The Service Worker
  3. The Service Worker Management, which takes care of the proper registration and installation of the service worker

The last part of the code shouldn’t be part of the app itself, in the meaning of SoC (Seperation of Concerns). It does not contribute to the functioning of the app.


The App

Here’s the general anatomy of my App:

app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var app = {
'settings': { // some stuff on app settings
...
},
'pages': { // views of the SPA
...
},
'ui': { // some UI helper
'dialog': function(msg, title) {
// show the message in a toast, popup or elsewhere
}
},
'starter': {
'init': { // initializing of the app
...
}
}
}

window.app = app;
app.starter.init();

Nothing unlikely, as I guess. It is built according to the composite pattern and has one entry point, that is called at the end of the file after it is instantiated.


The Service Worker

The Service Worker lives in its own JS file and its implementation is really straight forward:

service-worker.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var cacheName = 'my-apps-cache-v1.2.3';

var appFiles = [
// list of app relating files, that has to be handled
]

var excludeUrls = [
// list if URL's that shouldn't be handled
]

self.addEventListener('install', function(e) {
// on install, open the cache and add all appFiles
...

//activate immediatly and dont wait for connected clients to disconnect
self.skipWaiting();
}

self.addEventListener('activate', function(e) {
// remove old caches

//say to all clients: Now I'm responsible
self.clients.claim();
}

self.addEventListener('fetch', function(e) {
// intercept requests and serve the app files out of the cache
}

I won’t go into the depth of my implementation here now, since it doesn’t matter for the message exchange. It is only good for you to know that I have versioned the cache name to exchange it with new versions.


The Service Worker Management

Now the management code for the Service Worker. It has to be loaded with the app code, because it is client code and later on it needs knowledge of the app.

sw-management.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
if('serviceWorker' in navigator) {

navigator.serviceWorker.register('service-worker.js')
.then(function(registration) {

// detect Service Worker update available
registration.addEventListener('updatefound', function() {

if (registration.installing) {

// detect install of new Service Worker
registration.installing.addEventListener('statechange', function() {

if (registration.waiting) {

if (navigator.serviceWorker.controller) {
// there is an existing controller that has been updated
//TODO: Send a message to the app
} else {
// first install
}

}

}
}

}

}

}

Lets Communicate…

In this setup, the communication code can be implemented. Let’s do it as a round trip.

1. Client sends message to Service Worker

As sw-management.js represents our Service Worker management code, we add a little function to send a message in here:

sw-management.js
1
2
3
4
5
6
7
function sendMessageToServiceWorker(type, msg) {
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage(
{'type': type, 'msg': msg }
);
}
}

We define a type for the purpose of our communcation and a message itself. The function can be called wherever, like this:

1
sendMessageToServiceWorker("TEST", "Hey, Service Worker");

2. Service Worker receives the message

In the Service Worker code we need to add a recipient:

service-worker.js
1
2
3
4
5
6
7
self.addEventListener('message', function(event) { 
if (!event.data) return;

if (event.data.type === 'TEST') {
// do something regarding to the type and/or with the message
}
}

3. … and sends a message back to the client

What we want to do with the message depends what we want to achive, but in this example, just let’s greet back:

service-worker.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function sendMessageToClients(type, msg) {
// as the SW can control multiple clients, we have to catch them all
self.clients.matchAll({ includeUncontrolled: true })
.then(function(clients) {
for (const client of clients) {
client.postMessage(
{'type': type, 'msg': msg }
);
}
});
}

self.addEventListener('message', function(event) {
if (!event.data) return;

if (event.data.type === 'TEST') {
sendMessageToClients(event.data.type, 'Hi Clients...');
}
}

4. Client receives the answer from the Service Worker

In order to get messages from the Service Worker, we have to implement a receiver in the client also:

sw-management.js
1
2
3
4
5
6
7
8
9
10
if('serviceWorker' in navigator) {

//... 'navigator.serviceWorker.register' stuff

navigator.serviceWorker.addEventListener('message', function(event) {
if (event.data) {
// do something with the message from the Service Worker
}
});
}

5. … and shows it in the app

As I pointed out earlier, that the management code has to be loaded alongside with the app code, it’s a breeze to show the message:

app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var app = {
...

'ui': { // some UI helper
'dialog': function(msg, title) {
// show the message in a toast, popup or elsewhere
}
}

...
}

window.app = app;
...
sw-management.js
1
2
3
4
5
6
7
8
9
10
if('serviceWorker' in navigator) {

//... 'navigator.serviceWorker.register' stuff

navigator.serviceWorker.addEventListener('message', function(event) {
if (event.data) {
app.ui.dialog.info(event.data.msg, 'Service Worker says...');
}
});
}

The Update Message

With this infrastructure, everything is there to show a message, when the Service Worker is updated:

sw-management.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
if('serviceWorker' in navigator) {

navigator.serviceWorker.register('service-worker.js')
.then(function(registration) {

// detect Service Worker update available
registration.addEventListener('updatefound', function() {

if (registration.installing) {

// detect install of new Service Worker
registration.installing.addEventListener('statechange', function() {

if (registration.waiting) {

if (navigator.serviceWorker.controller) {
// there is an existing controller that has been updated

app.ui.dialog.info('New version installed', 'Service Worker');

} else {
// first install
}

}

}
}

}

}

...

}

Important to point out, that the Service Worker side of the communication is not involved in this case, because only the client-side management code knows when a new version has to be installed.


More Info

Syndicated at

Webmentions

No Webmentions yet...

In case your blog software can't send Webmentions, you can use this form to submit me a mention of this article...


Comments

Related