Comunicación bidireccional con los service workers

Andrew Guan
Andrew Guan
Demián Renzulli
Demián Renzulli

En algunos casos, es posible que una app web deba establecer un canal de comunicación de dos vías entre la página y el trabajador de servicio.

Por ejemplo, en una AWP de podcast, se podría compilar una función que permita al usuario descargar episodios para consumo sin conexión y permitir que el service worker mantenga la página informada periódicamente sobre el progreso, de modo que el subproceso principal pueda actualizar la IU.

En esta guía, exploraremos las diferentes formas de implementar una comunicación bidireccional entre el contexto de Window y el trabajador de servicio. Para ello, exploraremos diferentes APIs, la biblioteca Workbox y algunos casos avanzados.

Diagrama que muestra un trabajador de servicio y la página que intercambian mensajes.

Uso de Workbox

workbox-window es un conjunto de módulos de la biblioteca de Workbox que se diseñaron para ejecutarse en el contexto de la ventana. La clase Workbox proporciona un método messageSW() para enviar un mensaje al service worker registrado de la instancia y esperar una respuesta.

El siguiente código de página crea una nueva instancia de Workbox y envía un mensaje al trabajador de servicio para obtener su versión:

const wb = new Workbox('/sw.js');
wb.register();

const swVersion = await wb.messageSW({type: 'GET_VERSION'});
console.log('Service Worker version:', swVersion);

El service worker implementa un objeto de escucha de mensajes en el otro extremo y responde al service worker registrado:

const SW_VERSION = '1.0.0';

self.addEventListener('message', (event) => {
  if (event.data.type === 'GET_VERSION') {
    event.ports[0].postMessage(SW_VERSION);
  }
});

En el fondo, la biblioteca usa una API de navegador que revisaremos en la siguiente sección: Message Channel, pero abstrae muchos detalles de implementación, lo que facilita su uso, a la vez que aprovecha la compatibilidad con navegadores que tiene esta API.

Diagrama que muestra la comunicación bidireccional entre la página y el service worker, con una ventana de Workbox.

Cómo usar las APIs del navegador

Si la biblioteca de Workbox no es suficiente para tus necesidades, hay varias APIs de nivel inferior disponibles para implementar la comunicación "bidireccional" entre páginas y service workers. Tienen algunas similitudes y diferencias:

Similitudes:

  • En todos los casos, la comunicación comienza en un extremo a través de la interfaz postMessage() y se recibe en el otro extremo mediante la implementación de un controlador message.
  • En la práctica, todas las APIs disponibles nos permiten implementar los mismos casos de uso, pero algunas de ellas podrían simplificar el desarrollo en algunas situaciones.

Diferencias:

  • Tienen diferentes formas de identificar el otro lado de la comunicación: algunos usan una referencia explícita al otro contexto, mientras que otros pueden comunicarse de forma implícita a través de un objeto de proxy creado en cada lado.
  • La compatibilidad con los navegadores varía entre ellos.
Diagrama que muestra la comunicación bidireccional entre la página y el trabajador de servicio, y las APIs de navegador disponibles.

API de Broadcast Channel

Navegadores compatibles

  • Chrome: 54.
  • Edge: 79.
  • Firefox: 38.
  • Safari: 15.4.

Origen

La API de Broadcast Channel permite la comunicación básica entre contextos de navegación a través de objetos BroadcastChannel.

Para implementarlo, primero, cada contexto debe crear una instancia de un objeto BroadcastChannel con el mismo ID y enviar y recibir mensajes desde él:

const broadcast = new BroadcastChannel('channel-123');

El objeto BroadcastChannel expone una interfaz postMessage() para enviar un mensaje a cualquier contexto de escucha:

//send message
broadcast.postMessage({ type: 'MSG_ID', });

Cualquier contexto del navegador puede escuchar mensajes a través del método onmessage del objeto BroadcastChannel:

//listen to messages
broadcast.onmessage = (event) => {
  if (event.data && event.data.type === 'MSG_ID') {
    //process message...
  }
};

Como se ve, no hay una referencia explícita a un contexto en particular, por lo que no es necesario obtener primero una referencia al service worker o a cualquier cliente en particular.

Diagrama que muestra la comunicación bidireccional entre la página y el service worker, mediante un objeto Broadcast Channel.

La desventaja es que, en el momento de escribir este artículo, la API es compatible con Chrome, Firefox y Edge, pero otros navegadores, como Safari, aún no la admiten.

API del cliente

Navegadores compatibles

  • Chrome: 40.
  • Edge: 17.
  • Firefox: 44.
  • Safari: 11.1.

Origen

La API de cliente te permite obtener una referencia a todos los objetos WindowClient que representan las pestañas activas que controla el trabajador del servicio.

Dado que la página está controlada por un solo service worker, escucha y envía mensajes al service worker activo directamente a través de la interfaz serviceWorker:

//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
  }
};

De manera similar, el service worker escucha los mensajes implementando un objeto de escucha onmessage:

//listen to messages
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'MSG_ID') {
    //Process message
  }
});

Para comunicarse con cualquiera de sus clientes, el service worker obtiene un array de objetos WindowClient mediante la ejecución de métodos como Clients.matchAll() y Clients.get(). Luego, puede postMessage() cualquiera de ellos:

//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'});
  }
});
Diagrama que muestra un service worker que se comunica con un array de clientes.

Client API es una buena opción para comunicarse fácilmente con todas las pestañas activas de un service worker de una manera relativamente sencilla. Todos los navegadores principales admiten la API, pero es posible que no todos sus métodos estén disponibles, así que asegúrate de verificar la compatibilidad del navegador antes de implementarla en tu sitio.

Canal de mensajes

Navegadores compatibles

  • Chrome: 2.
  • Edge: 12.
  • Firefox: 41.
  • Safari: 5.

Origen

El canal de mensajes requiere definir y pasar un puerto de un contexto a otro para establecer un canal de comunicación bidireccional.

Para inicializar el canal, la página crea una instancia de un objeto MessageChannel y lo usa para enviar un puerto al service worker registrado. La página también implementa un objeto de escucha onmessage para recibir mensajes del otro contexto:

const messageChannel = new MessageChannel();

//Init port
navigator.serviceWorker.controller.postMessage({type: 'PORT_INITIALIZATION'}, [
  messageChannel.port2,
]);

//Listen to messages
messageChannel.port1.onmessage = (event) => {
  // Process message
};
Diagrama que muestra una página que pasa un puerto a un trabajador de servicio para establecer una comunicación bidireccional.

El service worker recibe el puerto, guarda una referencia a él y lo usa para enviar un mensaje al otro lado:

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'});

Actualmente, MessageChannel es compatible con todos los navegadores principales.

APIs avanzadas: Sincronización en segundo plano y actualización en segundo plano

En esta guía, exploramos formas de implementar técnicas de comunicación bidireccional para casos relativamente simples, como pasar un mensaje de cadena que describe la operación que se debe realizar o una lista de URLs para almacenar en caché de un contexto a otro. En esta sección, exploraremos dos APIs para controlar situaciones específicas: falta de conectividad y descargas largas.

Sincronización en segundo plano

Navegadores compatibles

  • Chrome: 49.
  • Edge: 79.
  • Firefox: No es compatible.
  • Safari: No se admite.

Origen

Una app de chat podría querer asegurarse de que los mensajes nunca se pierdan debido a una mala conectividad. La API de Background Sync te permite aplazar las acciones para que se vuelvan a intentar cuando el usuario tenga una conectividad estable. Esto es útil para garantizar que se envíe lo que el usuario quiera enviar.

En lugar de la interfaz postMessage(), la página registra un sync:

navigator.serviceWorker.ready.then(function (swRegistration) {
  return swRegistration.sync.register('myFirstSync');
});

Luego, el trabajador del servicio escucha el evento sync para procesar el mensaje:

self.addEventListener('sync', function (event) {
  if (event.tag == 'myFirstSync') {
    event.waitUntil(doSomeStuff());
  }
});

La función doSomeStuff() debe mostrar una promesa que indique el éxito o el error de lo que intenta hacer. Si se cumple, la sincronización se completa. Si falla, se programará otra sincronización para intentarlo nuevamente. Las sincronizaciones de reintento también esperan la conectividad y emplean una retirada exponencial.

Una vez que se realiza la operación, el trabajador del servicio puede volver a comunicarse con la página para actualizar la IU con cualquiera de las APIs de comunicación que se exploraron antes.

La Búsqueda de Google usa la sincronización en segundo plano para conservar las consultas que fallaron debido a una mala conectividad y volver a intentarlas más adelante cuando el usuario esté en línea. Una vez que se realiza la operación, comunican el resultado al usuario a través de una notificación push web:

Diagrama que muestra una página que pasa un puerto a un trabajador de servicio para establecer una comunicación bidireccional.

Recuperación en segundo plano

Navegadores compatibles

  • Chrome: 74.
  • Borde: 79.
  • Firefox: No es compatible.
  • Safari: No se admite.

Origen

Para tareas relativamente cortas, como enviar un mensaje o una lista de URLs para almacenar en caché, las opciones que se exploraron hasta ahora son una buena opción. Si la tarea tarda demasiado, el navegador finalizará el trabajador del servicio. De lo contrario, se corre el riesgo de dañar la privacidad y la batería del usuario.

La API de Background Fetch te permite transferir una tarea larga a un trabajador de servicio, como descargar películas, podcasts o niveles de un juego.

Para comunicarte con el trabajador de servicio desde la página, usa backgroundFetch.fetch en lugar de 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,
    },
  );
});

El objeto BackgroundFetchRegistration permite que la página escuche el evento progress para seguir el progreso de la descarga:

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}%`);
});
Diagrama que muestra una página que pasa un puerto a un service worker para establecer una comunicación bidireccional.
La IU se actualiza para indicar el progreso de una descarga (izquierda). Gracias a los trabajadores de servicio, la operación puede seguir ejecutándose cuando se cierran todas las pestañas (derecha).

Próximos pasos

En esta guía, exploramos el caso más general de comunicación entre la página y los trabajadores del servicio (comunicación bidireccional).

Muchas veces, es posible que solo se necesite un contexto para comunicarse con el otro, sin recibir una respuesta. Consulta las siguientes guías para obtener orientación sobre cómo implementar técnicas unidireccionales en tus páginas desde y hacia el servicio en segundo plano, junto con casos de uso y ejemplos de producción:

  • Guía de almacenamiento en caché imperativo: Llamar a un service worker desde la página para almacenar los recursos en caché con anticipación (p.ej., en situaciones de carga previa).
  • Broadcast updates: Llama a la página desde el service worker para informar actualizaciones importantes (p.ej., si hay una versión nueva disponible de la app web).