API de BroadcastChannel: un bus de mensajes para la Web

La API de BroadcastChannel permite que las secuencias de comandos del mismo origen envíen mensajes a otros contextos de navegación. Se puede considerar como un simple bus de mensajes que permite la semántica de Pub/Sub entre ventanas y pestañas, iframes, trabajadores web y service worker.

Conceptos básicos de API

La API de Broadcast Channel es una API simple que facilita la comunicación entre contextos de navegación. Es decir, comunicación entre ventanas/pestañas, iframes, trabajadores web y service workers. Los mensajes que se publican en un canal determinado se entregan a todos los oyentes de ese canal.

El constructor BroadcastChannel toma un solo parámetro: el nombre de un canal. El nombre identifica el canal y se publica en distintos contextos de navegación.

// Connect to the channel named "my_bus".
const channel = new BroadcastChannel('my_bus');

// Send a message on "my_bus".
channel.postMessage('This is a test message.');

// Listen for messages on "my_bus".
channel.onmessage = function(e) {
    console.log('Received', e.data);
};

// Close the channel when you're done.
channel.close();

Cómo mandar mensajes

Los mensajes pueden ser cadenas o cualquier cosa compatible con el algoritmo de clonación estructurada (Strings, Objetos, Arrays, Blobs, ArrayBuffer y Map).

Ejemplo: Envío de un BLOB o File

channel.postMessage(new Blob(['foo', 'bar'], {type: 'plain/text'}));

Un canal no se transmite a sí mismo. Por lo tanto, si tienes un objeto de escucha onmessage en la misma página que un postMessage() en el mismo canal, ese evento message no se activa.

Diferencias con otras técnicas

En este punto, es posible que te preguntes cómo se relaciona esto con otras técnicas de transmisión de mensajes, como WebSockets, SharedWorkers, la API de MessageChannel y window.postMessage(). La API de Broadcast Channel no reemplaza a estas APIs. Cada uno tiene un propósito. La API de Broadcast Channel está diseñada para facilitar la comunicación de uno a varios entre secuencias de comandos del mismo origen.

Estos son algunos casos de uso de los canales de transmisión:

  • Detecta acciones de usuarios en otras pestañas
  • Entérate cuando un usuario acceda a una cuenta en otra ventana o pestaña.
  • Indícale a un trabajador que realice tareas en segundo plano
  • Entérate cuando finalice una acción de un servicio.
  • Cuando el usuario sube una foto en una ventana, pásala a otras páginas abiertas.

Ejemplo: Una página que sabe cuándo el usuario sale, incluso desde otra pestaña abierta en el mismo sitio:

<button id="logout">Logout</button>

<script>
function doLogout() {
    // update the UI login state for this page.
}

const authChannel = new BroadcastChannel('auth');

const button = document.querySelector('#logout');
button.addEventListener('click', e => {
    // A channel won't broadcast to itself so we invoke doLogout()
    // manually on this page.
    doLogout();
    authChannel.postMessage({cmd: 'logout', user: 'Eric Bidelman'});
});

authChannel.onmessage = function(e) {
    if (e.data.cmd === 'logout') {
    doLogout();
    }
};
</script>

En otro ejemplo, supongamos que deseas indicarle a un service worker que quite el contenido almacenado en caché después de que el usuario cambie su "configuración de almacenamiento sin conexión" en tu app. Puedes borrar sus cachés con window.caches, pero es posible que el service worker ya contenga una utilidad para hacerlo. Podemos usar la API de Broadcast Channel para reutilizar ese código. Sin la API de Broadcast Channel, tendrías que aplicar un bucle a los resultados de self.clients.matchAll() y llamar a postMessage() en cada cliente para lograr la comunicación de un service worker con todos sus clientes (código real que lo hace). Usar un canal de transmisión hace que este sea O(1) en lugar de O(N).

Ejemplo: Indica a un service worker que quite una caché y vuelva a usar sus métodos de utilidad internos.

En index.html

const channel = new BroadcastChannel('app-channel');
channel.onmessage = function(e) {
    if (e.data.action === 'clearcache') {
    console.log('Cache removed:', e.data.removed);
    }
};

const messageChannel = new MessageChannel();

// Send the service worker a message to clear the cache.
// We can't use a BroadcastChannel for this because the
// service worker may need to be woken up. MessageChannels do that.
navigator.serviceWorker.controller.postMessage({
    action: 'clearcache',
    cacheName: 'v1-cache'
}, [messageChannel.port2]);

En sw.js

function nukeCache(cacheName) {
    return caches.delete(cacheName).then(removed => {
    // ...do more stuff (internal) to this service worker...
    return removed;
    });
}

self.onmessage = function(e) {
    const action = e.data.action;
    const cacheName = e.data.cacheName;

    if (action === 'clearcache') {
    nukeCache(cacheName).then(removed => {
        // Send the main page a response via the BroadcastChannel API.
        // We could also use e.ports[0].postMessage(), but the benefit
        // of responding with the BroadcastChannel API is that other
        // subscribers may be listening.
        const channel = new BroadcastChannel('app-channel');
        channel.postMessage({action, removed});
    });
    }
};

Diferencia con postMessage()

A diferencia de postMessage(), ya no necesitas mantener una referencia a un iframe o un trabajador para comunicarte con él:

// Don't have to save references to window objects.
const popup = window.open('https://another-origin.com', ...);
popup.postMessage('Sup popup!', 'https://another-origin.com');

window.postMessage() también te permite comunicarte a través de los orígenes. La API de Broadcast Channel es del mismo origen. Dado que se garantiza que los mensajes provendrán del mismo origen, no es necesario validarlos como solíamos hacerlo con window.postMessage():

// Don't have to validate the origin of a message.
const iframe = document.querySelector('iframe');
iframe.contentWindow.onmessage = function(e) {
    if (e.origin !== 'https://expected-origin.com') {
    return;
    }
    e.source.postMessage('Ack!', e.origin);
};

Simplemente "suscríbete" a un canal específico y ten una comunicación bidireccional segura.

Diferencia con SharedWorkers

Usa BroadcastChannel para casos simples en los que necesites enviar un mensaje a varias ventanas, pestañas o trabajadores.

Para casos de uso más sofisticados, como administrar bloqueos, estado compartido, sincronizar recursos entre un servidor y varios clientes o compartir una conexión WebSocket con un host remoto, los trabajadores compartidos son la solución más adecuada.

Diferencia con la API de MessageChannel

La diferencia principal entre la API de Channel Messaging y BroadcastChannel es que esta última es un medio para enviar mensajes a múltiples objetos de escucha (de uno a varios). MessageChannel está diseñado para la comunicación uno a uno directamente entre secuencias de comandos. También es más complejo, ya que requiere que configures canales con un puerto en cada extremo.

Detección de funciones y compatibilidad con navegadores

Actualmente, Chrome 54, Firefox 38 y Opera 41 admiten la API de Broadcast Channel.

if ('BroadcastChannel' in self) {
    // BroadcastChannel API supported!
}

En cuanto a los polyfills, hay algunos:

Como no los probé, el kilometraje puede variar.

Recursos