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 interpretar como un simple bus de mensajes que permite la semántica de Pub/Sub entre ventanas o pestañas, iframes, trabajadores web y service workers.

Conceptos básicos de API

La API de Broadcast Channel es una API simple que facilita la comunicación entre los contextos de navegación. Es decir, hay 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 al canal y se encuentra en todos los 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 elemento compatible con el algoritmo de clonación estructurada (cadenas, objetos, arrays, BLOB, ArrayBuffer y Map).

Ejemplo: envía un BLOB o File

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

Un canal no se transmite a sí mismo. Si tienes un objeto de escucha onmessage en la misma página que un postMessage() al 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 para pasar mensajes, como WebSockets, SharedWorkers, la API de MessageChannel y window.postMessage(). La API de Broadcast Channel no reemplaza estas APIs. Cada uno sirve para un propósito. La API de Broadcast Channel está diseñada para que la comunicación entre secuencias de comandos del mismo origen sea sencilla.

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

  • Detecta acciones de usuarios en otras pestañas
  • Entérate cuando un usuario accede a una cuenta en otra ventana o pestaña.
  • Indica a un trabajador que realice tareas en segundo plano
  • Saber cuándo un servicio termina de realizar alguna acción
  • Cuando el usuario sube una foto en una ventana, pásala a otras páginas abiertas.

Ejemplo: Una página que sabe cuando el usuario sale de su cuenta, 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 quieres indicarle a un service worker que quite contenido almacenado en caché después de que el usuario cambia 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 contienen una utilidad para hacerlo. Podemos usar la API de Broadcast Channel para y reutilizar ese código. Sin la API de Broadcast Channel, tendrías que repetir indefinidamente 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). Si usas un canal de transmisión, este elemento será O(1) en lugar de O(N).

Ejemplo: indica a un service worker que quite una caché y reutilice 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 tienes que 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 entre orígenes. La API de Broadcast Channel tiene el mismo origen. Dado que se garantiza que los mensajes provengan 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 particular y tengan una comunicación segura y bidireccional.

Diferencia con SharedWorkers

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

Para casos de uso más complejos, como administrar bloqueos, estado compartido, sincronizar recursos entre un servidor y varios clientes o compartir una conexión de 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 este último es un medio para enviar mensajes a varios objetos de escucha (uno a varios). MessageChannel está diseñado para la comunicación uno a uno directamente entre secuencias de comandos. También es más compleja, ya que se 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 son compatibles con la API de Broadcast Channel.

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

En cuanto a los polyfills, existen algunos:

No los probé, así que el kilometraje puede variar.

Recursos