PWA

4.2 Messaging and Screen APIs

Module Still Under Development

# Messaging

To be able to change the functionality of your Service Worker depending on whether or not the browser is online or offline, we need to first test for the online status and then pass that information between the webpage and the Service Worker.

More note notes about online and offline events are in Module 5.2.

Once you have a status for the webpage being online or offline (usually a boolean variable), then you need to pass a message from the page to the Service Worker with that status.

const APP = {
  //initial status when your page loads
  isOnline: 'onLine' in navigator && navigator.onLine,
  init() {
    //then use the online and offline events to change APP.isOnline
    //then send a message to the service worker with the new value
  },
};
1
2
3
4
5
6
7
8

Each script, the main and the Service Worker, should have their own variable that holds a value indicating the online status. Then we use one of the messaging techniques to pass that value between the two scripts.

There are several different ways to send messages from the script to the service worker. These techniques can also be used to send messages between open windows or tabs that belong to the same domain.

The message that you send between the scripts can be a String, Number, Boolean, null, Array or Object. Basically, anything that you can convert into or from JSON.

# Broadcast Messaging

Broadcast Messaging is like a radio station, which broadcasts on a specific frequency and any other page, tab, or worker from the same domain can listen for messages.

In your main script create a BroadcastChannel that you can use to send and receive messages from your script. Your channel object can listen for message events. Your channel object also has a postMessage() method that will broadcast your message.

//app.js

const APP = {
  channel: new BroadcastChannel('wkrp'),
  init() {
    //listen for broadcasted messages
    APP.channel.addEventListener('message', APP.gotMessage);
  },
  gotMessage(ev) {
    //this runs when receiving a message
    if (ev && ev.data) {
      console.log(ev.data.message);
    }
  },
  sendMessage() {
    //this runs when you need to send a message
    //messages can be any object that could be turned into JSON
    let theMessage = { message: 'Some information' };
    APP.channel.postMessage(theMessage);
  },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Broadcast Messaging can also be useful when you want to keep multiple webpages in sync and you are not using a service worker. In the situation where a user has multiple tabs open to the same website you can use Broadcast Messaging to tell all the other tabs when something happens on the current tab. Combine this with the Page Visibility API to manage your web app.

# Channel Messaging

Broadcast messaging is sending a message out to anyone who is listening within the browser on the same domain. Channel messaging is a way to create a pair of clients that can talk to one another. For example, a main script and a service worker.

This process starts with the main script creating a MessageChannel object.

//app.js

const APP = {
  channel: new MessageChannel(),
};
1
2
3
4
5

Next you need to access the two port objects inside that MessageChannel. port1 is used by the main script to receive messages from the service worker (or other tab|window). port2 is sent to the service worker from the main script with a message direct to the active service worker.

//app.js

//after registering the service worker, get the active worker
navigator.serviceWorker.ready.then((reg) => {
  //send the port2 to the sw inside the second parameter in an array
  //the array will be accessed as a property called `ports`
  reg.active.postMessage({ type: 'PORT' }, [APP.channel.port2]);
  //the message object {type:'PORT'} is just an identifier for the service worker
  //so it knows it is receiving a port object
});
1
2
3
4
5
6
7
8
9
10

In the service worker you will receive this message with the standard message event listener and then set up the listener for the port that is passed in.

//sw.js
self.addEventListener('message', (ev) => {
  // console.log(ev.data);
  console.log('message received in sw');
  if (ev.data.type && ev.data.type === 'PORT') {
    console.log(ev.ports); //the array from the postMessage() method
    port = ev.ports[0];
    sendMessage('ports received');
    //now add a listener to the port
    port.onmessage = gotMessage;
  }
});
1
2
3
4
5
6
7
8
9
10
11
12

The listener in your main script, for incoming messages from the service worker, is put on port1.

//app.js

APP.channel.port1.onmessage = APP.gotMessage;
//gotMessage is our function that listens for messages from the service worker

//...

gotMessage(ev){
  if(ev.data.message ){
    //message received from service worker
  }
},
1
2
3
4
5
6
7
8
9
10
11
12

# Client Messaging

The third way to send messages is by using the postMessage() method of the Client interface. this is actually what we did above to pass the port2 object from the main script to the service worker.

Sending messages from the main script to the service worker is just this:

//app.js

navigator.serviceWorker.ready.then((reg) => {
  //the messageObj is your object that gets passed to the service worker
  reg.active.postMessage(messageObj);
});
1
2
3
4
5
6

In the service worker it will be received as a property called data on the message event.

//sw.js
self.addEventListener('message', (ev) => {
  let messageObj = ev.data;
  //all client messages are received like this inside the service worker
  console.log(ev.source.id);
  //ev.source.id is the id of the window client who sent the message to the service worker
});
1
2
3
4
5
6
7

Any tab from the same origin can send a message to the service worker with the postMessage method. When the service worker receives the message event, then the ev.data property is the message object that was sent from the window. The ev.source.id property is the client id of the window which sent the message to the service worker.

To send a message using Client messaging is a bit more work.

When you have a fetch event occur in the service worker there should be a property on the event called ev.clientId. This is the identifying id for the window client that sent the message (from your main script).

self.addEventListener('fetch', async (ev) => {
  //works for `message` event too
  //after doing other things... if you want to send a message
  if (!ev.clientId) {
    // Get the client.
    const client = await clients.get(ev.clientId);
    let message = { type: 'hello' };
    sendMessage(message, client);
  }
});

function sendMessage(msg, client) {
  //this function will send a message to ONE specific client
  client.postMessage(msg);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

If you want to get the single clientId from the event then the property that you use depends on which event it is. The fetch event has a clientId property. The message event has a source property that contains an id property with the clientId.

self.addEventListener('fetch', (ev) => {
  console.log(ev.clientId);
});

self.addEventListener('message', (ev) => {
  console.log(ev.source.id);
});
1
2
3
4
5
6
7

The challenging part comes when you want to send a message to ALL the connected clients.

It is very possible for a user to have multiple tabs open for the same website. All these tabs will be sharing the same service worker. If you need to keep things in sync across all those tabs, then it is very likely that you will need to send the same message to all the tabs from the service worker at the same time. Each of those tabs will be running their own copy of the main script. They will each receive and handle their own copy of the message.

But how do we send a message to each of the tabs (clients)?

//sw.js
//send a message to all connected clients
function sendMessage(msg) {
  //same message being sent to all clients
  //clients.matchAll() will find all the clients of this service worker
  clients.matchAll().then((clientList) => {
    for (const client of clientList) {
      //loop through each of the clients
      console.log(client.id, client.type, client.url);
      //id - the unique id for the client
      //type - window, worker, or sharedworker
      //url - url of the client window
      client.postMessage(msg);
    }
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

The clients.matchAll() method can be passed an options object with a type property. The default is {type: 'window'} but you can change it to all or worker or sharedworker.

If you only want to send the message to selected window clients, then you can compare the the fetch event's ev.clientId or the message event's ev.source.id against the list of window client ids that are sharing the service worker.

//msg is the message object to send
//clientID is the client id coming from the message or fetch event
function sendMessage(msg, clientID) {
  clients.matchAll().then((clientList) => {
    for (const client of clientList) {
      if (client.id === clientID) {
        //replying to one specific client id
        client.postMessage('back to the window that triggered the fetch or message event');
      } else {
        //replying to all the other clients
        client.postMessage('to all windows except the one that triggered the fetch or message event');
      }
    }
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# ControllerChange vs ready Property

In some of the videos and code samples you will see reference to using a controller object to send messages and listening for the controllerchange event. Consider this the older approach.

//old approach
navigator.serviceWorker.addEventListener('controllerchange', (ev) => {
  //this means that there has been a change in which service worker is controlling the webpage
  //and you now need to change to the new controller to be able to send messages
});
1
2
3
4
5

The better approach for sending a message is to use the ready property to get the current active service worker and send the message. The ready property is a Promise. So, you can chain a then() method on to it which will call a function as soon as there is any available active service worker. The registration object will be passed to the then function.

//recommended approach to sending a message to a service worker
function sendMessage(msg) {
  navigator.serviceWorker.ready.then((reg) => {
    //we have access to the active service worker - reg.active
    reg.active.postMessage(msg);
  });
}
1
2
3
4
5
6
7

This is the recommended approach to sending a message from the main script to the service worker.

# Page Visibility API

As a user switches between tabs or open windows, you can use the Page Visibility API to keep track of when your page comes into focus or loses focus.

Page Visibility API reference (opens new window)

# FullScreen API

To manage the permissions and toggling in and out of fullscreen mode, you can use the FullScreen API.

FullScreen API reference (opens new window)

# ScreenOrientation API

If you need to track the screen orientation - portrait or landscape - then you can use the ScreenOrientation API to listen for the user rotating their device to change the orientation. It can also be triggered by the user resizing their laptop screen.

Do NOT confuse this API with the Device Orientation API which will give you the angle of rotation of the device over the X-axis, Y-axis, and Z-axis.

Screen Orientation API reference (opens new window)

Device Orientation API reference (opens new window)

# ToDo This Week

To Do this week

Last Updated: 5/2/2024, 4:15:53 PM