In some cases, a web app might need to establish a two-way communication channel between the page and the service worker.
For example: in a podcast PWA one could build a feature to let the user download episodes for offline consumption and allow the service worker to keep the page regularly informed about the progress, so the main thread can update the UI.
In this guide we'll explore the different ways of implementing a two-way communication between the Window and service worker context, by exploring different APIs, the Workbox library, as well as some advanced cases.
Using Workbox
workbox-window
is a set of
modules of the Workbox library that are intended
to run in the window context. The Workbox
class provides a messageSW()
method to send a message to the instance's registered service worker and
await a response.
The following page code creates a new Workbox
instance and sends a message to the service worker
to obtain its version:
const wb = new Workbox('/sw.js');
wb.register();
const swVersion = await wb.messageSW({type: 'GET_VERSION'});
console.log('Service Worker version:', swVersion);
The service worker implements a message listener on the other end, and responds to the registered service worker:
const SW_VERSION = '1.0.0';
self.addEventListener('message', (event) => {
if (event.data.type === 'GET_VERSION') {
event.ports[0].postMessage(SW_VERSION);
}
});
Under the hood the library uses a browser API that we'll review in the next section: Message Channel, but abstracts many implementation details, making it easier to use, while leveraging the wide browser support this API has.
Using Browser APIs
If the Workbox library is not enough for your needs, there are several lower-level APIs available to implement "two-way" communication between pages and service workers. They have some similarities and differences:
Similarities:
- In all cases the communication starts on one end via the
postMessage()
interface and is received on the other end by implementing amessage
handler. - In practice, all the available APIs allow us to implement the same use cases, but some of them might simplify development in some scenarios.
Differences:
- They have different ways of identifying the other side of the communication: some of them use an explicit reference to the other context, while others can communicate implicitly via a proxy object instantiated on each side.
- Browser support varies among them.
Broadcast Channel API
The Broadcast Channel API allows basic communication between browsing contexts via BroadcastChannel objects.
To implement it, first, each context has to instantiate a BroadcastChannel
object with the same ID
and send and receive messages from it:
const broadcast = new BroadcastChannel('channel-123');
The BroadcastChannel object exposes a postMessage()
interface to send a message to any listening
context:
//send message
broadcast.postMessage({ type: 'MSG_ID', });
Any browser context can listen to messages via the onmessage
method of the BroadcastChannel
object:
//listen to messages
broadcast.onmessage = (event) => {
if (event.data && event.data.type === 'MSG_ID') {
//process message...
}
};
As seen, there's no explicit reference to a particular context, so there's no need of obtaining a reference first to the service worker or any particular client.
The disadvantage is that, at the moment of this writing, the API has support from Chrome, Firefox and Edge, but other browsers, like Safari, don't support it yet.
Client API
The Client API allows you to obtain a
reference to all the WindowClient
objects representing the active tabs that the service worker is controlling.
Since the page is controlled by a single service worker, it listens to and sends messages to the
active service worker directly via the serviceWorker
interface:
//send message
navigator.serviceWorker.controller.postMessage({
type: 'MSG_ID',
});
//listen to messages
navigator.serviceWorker.onmessage = (event) => {
if (event.data && event.data.type === 'MSG_ID') {
//process response
}
};
Similarly, the service worker listens to messages by implementing an onmessage
listener:
//listen to messages
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'MSG_ID') {
//Process message
}
});
To communicate back with any of its clients, the service worker obtains an array of
WindowClient
objects by executing
methods such as
Clients.matchAll()
and
Clients.get()
. Then it can
postMessage()
any of them:
//Obtain an array of Window client objects
self.clients.matchAll(options).then(function (clients) {
if (clients && clients.length) {
//Respond to last focused tab
clients[0].postMessage({type: 'MSG_ID'});
}
});
Client API
is a good option to communicate easily with all the active tabs from a service worker
in a relatively straightforward way. The API is supported by all major
browsers,
but not all its methods might be available, so make sure to check browser support before
implementing it in your site.
Message Channel
Message Channel requires defining and passing a port from one context to another to establish a two-way communication channel.
To initialize the channel, the page instantiates a MessageChannel
object and uses it
to send a port to the registered service worker. The page also implements an onmessage
listener on
it to receive messages from the other context:
const messageChannel = new MessageChannel();
//Init port
navigator.serviceWorker.controller.postMessage({type: 'PORT_INITIALIZATION'}, [
messageChannel.port2,
]);
//Listen to messages
messageChannel.port1.onmessage = (event) => {
// Process message
};
The service worker receives the port, saves a reference to it and uses it to send a message to the other side:
let communicationPort;
//Save reference to port
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'PORT_INITIALIZATION') {
communicationPort = event.ports[0];
}
});
//Send messages
communicationPort.postMessage({type: 'MSG_ID'});
MessageChannel
is currently supported by all major
browsers.
Advanced APIs: Background Sync and Background Fetch
In this guide we explored ways of implementing two-way communication techniques, for relatively simple cases, like passing a string message describing the operation to perform, or a list of URLs to cache from one context to the other. In this section we'll explore two APIs to handle specific scenarios: lack of connectivity and long downloads.
Background Sync
A chat app might want to make sure that messages are never lost due to bad connectivity. The Background Sync API lets you defer actions to be retried when the user has stable connectivity. This is useful for ensuring that whatever the user wants to send, is actually sent.
Instead of the postMessage()
interface, the page registers a sync
:
navigator.serviceWorker.ready.then(function (swRegistration) {
return swRegistration.sync.register('myFirstSync');
});
The service worker then listens for the sync
event to process the message:
self.addEventListener('sync', function (event) {
if (event.tag == 'myFirstSync') {
event.waitUntil(doSomeStuff());
}
});
The function doSomeStuff()
should return a promise indicating the success/failure of whatever it's
trying to do. If it fulfills, the sync is complete. If it fails, another sync will be scheduled to
retry. Retry syncs also wait for connectivity, and employ an exponential back-off.
Once the operation has been performed, the service worker can then communicate back with the page to update the UI, by using any of the communication APIs explored earlier.
Google search uses Background Sync to persist failed queries due to bad connectivity, and retry them later when the user is online. Once the operation is performed, they communicate the result to the user via a web push notification:
Background Fetch
For relatively short bits of work like sending a message, or a list of URLs to cache, the options explored so far are a good choice. If the task takes too long the browser will kill the service worker, otherwise it's a risk to the user's privacy and battery.
The Background Fetch API allows you to offload a long task to a service worker, like downloading movies, podcasts, or levels of a game.
To communicate to the service worker from the page, use backgroundFetch.fetch
, instead of
postMessage()
:
navigator.serviceWorker.ready.then(async (swReg) => {
const bgFetch = await swReg.backgroundFetch.fetch(
'my-fetch',
['/ep-5.mp3', 'ep-5-artwork.jpg'],
{
title: 'Episode 5: Interesting things.',
icons: [
{
sizes: '300x300',
src: '/ep-5-icon.png',
type: 'image/png',
},
],
downloadTotal: 60 * 1024 * 1024,
},
);
});
The BackgroundFetchRegistration
object allows the page listen to the progress
event to follow
the progress of the download:
bgFetch.addEventListener('progress', () => {
// If we didn't provide a total, we can't provide a %.
if (!bgFetch.downloadTotal) return;
const percent = Math.round(
(bgFetch.downloaded / bgFetch.downloadTotal) * 100,
);
console.log(`Download progress: ${percent}%`);
});
Next steps
In this guide we explored the most general case of communication between page and service workers (bidirectional communication).
Many times, one might need only one context to communicate with the other, without receiving a response. Check out the following guides for guidance how to implement unidirectional techniques in your pages from and to the service worker, along with use cases and production examples:
- Imperative caching guide: Calling a service worker from the page to cache resources in advance (e.g. in prefetching scenarios).
- Broadcast updates: Calling the page from the service worker to inform about important updates (e.g. a new version of the webapp is available).