14

I try to postMessage between a web application and the corresponding service worker. The service worker is successfully registered and working so far. Unfortunately, I noticed some strange behavior:

  1. The navigator.serviceWorker.controller is always null.
  2. At the service worker side, I implemented postMessage as follows:
self.addEventListener('message', function (evt) {
    console.log('postMessage received', evt);
});

Unfortunately, the important fields to post back to the origin evt.origin='' and evt.source=null do not contain the desired values. Nonetheless, I always received the sent evt.data.

Do you know how to post back?

Thank you very much!
Andi

1
  • I have the same issue where navigator.serviceWorker.controller is always null. The selected answer didn't seem to resolve that issue - how did you resolve it? Commented Mar 1, 2016 at 15:44

4 Answers 4

21

One way to send a response from the worker back to the controlled app is shown in this demo and is done the following way (I haven't actually tested this yet, but the demo works well and the code seems to agree with the specs).

In the main page:

function sendMessage(message) {
  // This wraps the message posting/response in a promise, which will
  // resolve if the response doesn't contain an error, and reject with
  // the error if it does. If you'd prefer, it's possible to call
  // controller.postMessage() and set up the onmessage handler
  // independently of a promise, but this is a convenient wrapper.
  return new Promise(function(resolve, reject) {
    var messageChannel = new MessageChannel();
    messageChannel.port1.onmessage = function(event) {
      if (event.data.error) {
        reject(event.data.error);
      } else {
        resolve(event.data);
      }
    };
    // This sends the message data as well as transferring
    // messageChannel.port2 to the service worker.
    // The service worker can then use the transferred port to reply
    // via postMessage(), which will in turn trigger the onmessage
    // handler on messageChannel.port1.
    // See
    // https://html.spec.whatwg.org/multipage/workers.html#dom-worker-postmessage
    navigator.serviceWorker.controller.postMessage(message, [messageChannel.port2]);
  });
}

And in the service worker code:

self.addEventListener('message', function(event) {
  event.ports[0].postMessage({'test': 'This is my response.'});
});

You should then be able to use the promise returned by the sendMessage function to do what you want with the response coming from the service worker.

3
  • How about sending message to multiple clients? For example in case of the init message that says that all scripts were parsed and executed? Commented Jun 10, 2016 at 13:54
  • 4
    Is there a way to do one way communication without sending the first message - I want to send a message on each 'install' event from the service worker to the client (without the client needs to send the first message)
    – Gal Bracha
    Commented Aug 9, 2016 at 19:14
  • In Firefox if you do a force refresh - ctrl shift r - the "controller" is always null. Commented Jun 29, 2022 at 3:03
12

@GalBracha was asking if you could communicate with the client without first sending a message - yes!. Here's a quick example of how I did it when I wanted to send a message to clients when a push notification was received (not when the user clicked on the notification, but when the service worker received the event):

in your client js (after service worker registration, etc):

// navigator.serviceWorker.addEventListener('message', ... ) should work too
navigator.serviceWorker.onmessage = function (e) {
    // messages from service worker.
    console.log('e.data', e.data);
};

in your service worker, in response to some event (perhaps the install event):

// find the client(s) you want to send messages to:
self.clients.matchAll(/* search options */).then( (clients) => {
    if (clients && clients.length) {
        // you need to decide which clients you want to send the message to..
        const client = clients[0];
        client.postMessage("your message");
    }

the trick is (obviously?) to attach your listener for the message event to the serviceWorker object, not the window object.

2
  • 2
    @GalBracha : See also stackoverflow.com/a/35108844/271577 (if posting back to a (non-controlled) registering page, the includeUncontrolled option can help.) Commented Jun 8, 2017 at 9:57
  • 1
    Love this! Also, it's much easier than that other site makes it out to be to send replys: developer.mozilla.org/en-US/docs/Web/API/… MessageEvent.source is the Client that sent the message. Obviously the source can be saved into an array as part of an object containing state information and such. Commented Jun 18, 2020 at 4:46
9

Broadcast Channel API is easier

Sending information back and forth between a service worker and its client is much easier using the Broadcast Channel API, compared to other methods.

Here is how broadcasting works

Define a new broadcasting channel in both the service worker and the client.

const channel4Broadcast = new BroadcastChannel('channel4');

To send a broadcast message in either the worker or the client:

channel4Broadcast.postMessage({key: value});

To receive a broadcast message in either the worker or the client:

channel4Broadcast.onmessage = (event) => {
    value = event.data.key;
}

Broadcasting also allows sending messages between web workers of the client, the client and the service worker.

Caveat

This simplicity comes at a price though. As it currently stands, the service worker will not wake up to receive broadcast messages. Therefore, it only works when the service worker is in a running (active or waiting) state; i.e. when the client is visible. Nonetheless, this may still be useful, for example to broadcast version numbers or web pushes, etc.

4

Turns out that navigator.serviceWorker.registration.active supports postMessage() while navigator.serviceWorker.controller stays null.

From this article with a working example: https://theshagod.medium.com/using-service-workers-and-postmessage-beginners-guide-488f99b1a28b

// paste the below code in your main.js
// in the page being controlled
if (navigator.serviceWorker) {

  navigator.serviceWorker.register('service-worker.js');

  navigator.serviceWorker.addEventListener('message', event => {
    // event is a MessageEvent object
    console.log(`The service worker sent me a message: ${event.data}`);
  });

  navigator.serviceWorker.ready.then( registration => {
    registration.active.postMessage("Hi service worker");
  });

}

Not the answer you're looking for? Browse other questions tagged or ask your own question.