As it turns out, there’s many different ways in which you can share messages between a ServiceWorker
and the web pages it controls.
- A client may want to message a
ServiceWorker
, asking something or notifying about something – this is “unicast” (1-to-1) - A
ServiceWorker
may want to send a reply to a client – unicast - A
ServiceWorker
may want to send an update to every client under its control – a “broadcast” (1-to-many) message - A
ServiceWorker
may want to send an update to the client where afetch
request originated – unicast
Messaging the ServiceWorker
In order to message a ServiceWorker
, you can use a piece of code like the following snippet.
worker.postMessage(data);
By default you probably want to use navigator.serviceWorker.controller
, the active ServiceWorker instance for the current page that controls its requests, as the worker
. I haven’t personally found a need yet for talking to workers other than the controller, which is why that’s the default worker
you talk to in swivel
. That being said, there are use cases for them, which is why there’s swivel.at(worker)
, but let’s go back to our focus area.
The worker can set up an event handler that deals with the data
payload we posted from the web page.
self.addEventListener('message', function handler (event) {
console.log(event.data);
});
As soon as you have different types of messages you want to send to workers, this becomes an issue. You’ll have to turn to a convention for message routing. A common convention is to define an envelope for your messages that has a command
property in it, so now you’d send messages like the following.
worker.postMessage({ command: 'deleteCache', key: key });
Then the worker needs to be updated with a command handling router, a bunch of if
will do in the simpler case. You can see how the code starts becoming diluted with implementation details.
self.addEventListener('message', function handler (event) {
if (event.data.command === 'deleteCache') {
caches.delete(event.data.key);
}
});
Getting Replies from the ServiceWorker
If you want the worker to be able to reply to the message things get very ugly, very quickly, mostly because browsers haven’t implemented the final API quite yet.
For the time being, on the client-side you’ll need to set up a MessageChannel
, bind a listener to port1
and pass along port2
when posting the message.
var messageChannel = new MessageChannel();
messageChannel.port1.addEventListener('message', replyHandler);
worker.postMessage(data, [messageChannel.port2]);
function replyHandler (event) {
console.log(event.data); // this comes from the ServiceWorker
}
On the ServiceWorker
side, it’s not that fun either. You have to reference port2
of the messageChannel
using event.ports[0]
, as its the port in position zero of the ports
passed along with the message.
self.addEventListener('message', function handler (event) {
event.ports[0].postMessage(data); // handle this using the replyHandler shown earlier
});
Browsers will eventually have an event.source
alternative to event.ports[0]
on the ServiceWorker
side that doesn’t need us to do any of the MessageChannel
stuff on the pages. Unfortunately that’s not here yet, and so we have to resort to MessageChannel
for now.
Broadcasting from a ServiceWorker to every client
This one is straightforward, but it’s also pretty different from the two situations we’ve just talked about. And, quite honestly, very verbose. Of course, ServiceWorker
is all about expressiveness and being able to cater for multiple different use cases, and we’ve all seen the veiled evil in seemingly simple but cleverly complicated interfaces like the AppCache manifest.
That being said, having to type this out sucks. Libraries will definitely help abstract the pain away, while ServiceWorker
can go on being just about the most powerful feature the modern web has to offer.
self.clients.matchAll().then(all => all.map(client => client.postMessage(data)));
To listen to these messages from a ServiceWorker
, you can register an event listener like below in your web page. Note how the listener is added on navigator.serviceWorker
and not on an specific worker
(like navigator.serviceWorker.controller
).
navigator.serviceWorker.addEventListener('message', function handler (event) {
console.log(event.data);
});
You probably want to keep a reference to the worker
you’re interested in, and then filter on the message listener by event.source
. That way you’ll avoid messages broadcasted from workers other than the one you’re expecting messages from.
navigator.serviceWorker.addEventListener('message', function handler (event) {
if (event.source !== worker) {
return;
}
console.log(event.data);
});
Dual Channeling fetch
Requests
Sending updates to the origin of fetch
requests is, perhaps, the most interesting use case for communication between ServiceWorker
and web pages. Sending an update to the origin of a fetch
request is what makes using the cache immediately and then sending an update as soon as possible so effective.
self.on('fetch', function handler (event) {
// reply to client here
});
Eventually, event
will have a clientId
property identifying the client where the request came from. We could then use code like below to send a message to the client
using client.postMessage
. No browser implements event.clientId
yet.
self.on('fetch', function handler (event) {
event.respondWith(caches.match(event.request));
fetch(event.request).then(response => response.json()).then(function (data) {
self.clients.match(event.clientId).then(client => client.postMessage(data));
});
});
You could still use the broadcasting mechanism discussed earlier, but it’d go to every client and not just the origin of the fetch
event. A better alternative, for now, may be to issue another request from the page after load, maybe adding a custom header indicating that we should force a fetch
on the ServiceWorker
side.
Swivel Makes Your Life Easier
If you’d prefer to avoid all of this frivolous knowledge, you may like to swivel
like Ron Swanson.
Swivelling has a number of benefits. The API is unified under an event emitter style. On the client, you can send messages to the ServiceWorker
like below.
swivel.emit('remove-cache', 'v1');
Then on the worker, you could just listen for that with a matching API.
swivel.on('remove-cache', (context, key) => caches.delete(key));
If the worker needs to reply, it can use context.reply
.
swivel.on('remove-cache', function handler (context, key) {
caches.delete(key).then(function () {
context.reply('removed-cache', 'ok', 'whatever');
});
});
The client that sent the message then gets to handle the reply as long as they’re listening for the removed-cache
event. Note how the API here is identical to the API for listening on events on the ServiceWorker.
swivel.on('removed-cache', function handler (context, success, metadata) {
// do something else
});
When ServiceWorker
has important announcements, it can broadcast to every client.
swivel.broadcast('announcement', { super: '!important' });
Broadcasted messages can be listened using the exact same swivel.on
API in the client-side. In addition to swivel.on
, there’s also swivel.once
that binds one time event handlers, and swivel.off
to remove event handlers.
Lastly, as we mentioned earlier you can interact with different ServiceWorker instances. Instead of using the swivel.*
API directly, you could use swivel.at(worker).*
instead. Messages sent from a client using swivel.at(worker).emit
will only go to worker
, and messages broadcasted by worker
will only be available to listeners registered using swivel.at(worker).on
. This kind of scoping helps prevent accidents when phasing out old workers and when installing new ones.
You can check out the full documentation on GitHub.
ServiceWorker
,MessageChannel
, &postMessage
. Oh, my!
Comments