Bus de Eventos Nativo en Javascript ( BroadcastChannel )


portada

Creamos un nuevo bus de eventos con el recurso nativo BroadcastChannel


En un artículo anterior vimos cómo crear un bus de eventos con javascript mediante el patrón observable. En este artículo vamos a ver cómo crear un Bus de Eventos a partir del recurso nativo BroadcastChannel.

BroadcastChannel es una clase que nos ayuda a crear una comunicación transversal entre elementos que quieran escuchar los eventos que lancen entre ellos. Al igual que vimos en el artículo anterior, los eventos se propagan y se capturan de tal manera que los elementos que no se encuentran en el camino de esa propagación o captura no pueden escuchar y reaccionar a esos eventos. Para ello vimos que podíamos, o bien capturar el evento primario en el document para después propagar otro evento que pudiera ser escuchado por el otro elemento, o bien implementar un bus de eventos que pudiera crear una comunicación entre los dos elementos mediante el patrón observable.

En este nuevo artículo vamos a ver como, mediante la clase BroadcastChannel que forma parte del API de javascript, podemos realizar esta comunicación de bus de eventos.


BroadcastChannel extende más allá que la simple comunicación entre los diferentes elementos del DOM. Esta API sirve también para establecer comunicaciones con diferentes contextos de navegación. Es decir, la comunicación entre ventanas y pestañas, iframes, web workers y serviceWorker. Los mensajes que se publican en un canal determinado se entregan a todos los oyentes de ese canal.


El canal se crea o se suscribe si ya existe al instanciar la clase mediante el parámetro de entrada al constructor:


const bc = new BroadcastChannel('nombre_canal');

Para escuchar ese canal necesitamos otra instancia de clase, ya el API no escucha los eventos lanzados desde el mismo objeto:



    const bc2 = new BroadcastChannel('nombre_canal');

    bc.onmessage = (e)=>{
        console.log(e.data);
    }

Para lanzar un evento en el canal podemos podemos realizarlo con el primer objeto o instanciando uno nuevo. Siempre que al crearlo de pasemos el nombre del canal se lanzarán todas las funciones registradas en las propiedades onmessage de todos los objetos instanciados menos en el que lanzó el evento.



    const bc = new BroadcastChannel('canal_1');
    const bc2 = new BroadcastChannel('canal_1');
    bc.onmessage = (event) =>{
        console.log(event.data)
    }
    bc2.postMessage('Hola');
    

Vamos a crear un ejemplo sencillo. Creamos un archivo html el siguiente código:



    <!DOCTYPE html>

    <html>
    
    <head>
        <meta charset='utf-8'>
        <meta http-equiv='X-UA-Compatible' content='IE=edge'>
        <title>Page Title</title>
        <meta name='viewport' content='width=device-width, initial-scale=1'>
    
    </head>
    <body>
        <div>
             <button id="boton">pulsar</button>
        </div>
        <div>
            <span id="texto"></span>
        </div>
        <script>
            const boton = document.querySelector('#boton');
            const texto = document.querySelector('#texto');
            boton.onclick = (e)=>{
                const bc = new BroadcastChannel('cambiar_texto');
                bc.postMessage('Hola');
                bc.close();
            }
            const bcc = new BroadcastChannel('cambiar_texto');
            bcc.onmessage = (e)=>{
                texto.innerText = e.data; 
            }
        </script>
    </body>
    </html>

Para ello, hemos implementado dentro de listener del evento click un nuevo objeto broadcast que tiene por nombre de canal “cambiar_texto”. Como también hemos declarado otro broadcast y a éste le hemos implementado la función de escucha, cuando se lance el evento será escuchado y cambiará el contenido del span. Puedes probar a crear varios objetos broadcast y crear distintos canales y observar cómo se comunican entre sí.


ejemplo

También se puede ver que hemos cerrado el objeto que lanza el evento. Esto es una buena práctica ya que estamos creando un objeto cada vez que se hace click. Podíamos haberlo en el ámbito padre pero de esta forma observamos mejor su independencia. También tenemos otros métodos de control de errores pero acabamos de ver lo principal para utilizar este API.


Pero en un desarrollo real puede que nos interese tener un servicio que se instancie una vez y podamos lanzar y escuchar eventos de ese canal creando un código más contenido. Podemos crear una clase e implementar toda la lógica de manejo dentro de ella.



    <!DOCTYPE html>

    <html>
    
    <head>
        <meta charset='utf-8'>
        <meta http-equiv='X-UA-Compatible' content='IE=edge'>
        <title>Page Title</title>
        <meta name='viewport' content='width=device-width, initial-scale=1'>
    
    </head>
    <body>
        <div>
             <button id="boton">pulsar</button>
        </div>
    
        <script>
            const boton = document.querySelector('#boton');
    
            class BroadcastService {
                static broadcastMap = new Map();
                constructor(chanel){
                    this.chanel = chanel;
                    this.onMessages = new Set();
    
                    if(!BroadcastService.broadcastMap.get(chanel)){
                        const bc = new  BroadcastChannel(chanel);
                        const objectService = {
                            bc: bc,
                            messages: this.onMessages
                        }
                        bc.onmessage = (e)=>{
                            objectService.messages.forEach(fn => {
                                fn(e.data);
                            });
                        }
                        BroadcastService.broadcastMap.set(chanel, objectService);
                    }else{
                       const objectService = BroadcastService.broadcastMap.get(chanel);
                        objectService.messages = new Set([...objectService.messages, ...this.onMessages]);
                    };  
                }
                postMessage(data){
                    const bc = new BroadcastChannel(this.chanel);
                    bc.postMessage(data);
                    bc.close();
                }
                onmessage(fn){
                    this.onMessages.add(fn);
                }   
            }
    
            const bcService = new BroadcastService('escribir_encosola');
            bcService.onmessage((e)=>{
                console.log('hola ' + e);
            });
            bcService.onmessage((e)=>{
                console.log('¿Qué tal estás?');
            });
            boton.onclick = ()=>{
                const bc = new BroadcastChannel('cambiar_texto');
                bc.postMessage('Hola');
                bc.close();
                const bcService = new BroadcastService('escribir_encosola');
                bcService.postMessage('Amigo');
            }
        </script>
    </body>
    </html>

Lo que acabamos de hacer es crear una clase denominada servicio. Podemos crear una o varias instancias de ella pero la idea es que se instancie una vez. Podría ser parte de un servicio tipo Angular controlado mediante inyección de dependencias e inversión de control. después tenemos el método postMessage que reúne la lógica vista antes. Esta es, crea una instancia del mismo canal, lanza el evento y lo cierra. Después tenemos la función onmessage que va recogiendo cada una de las funciones pasadas por parámetro. Por último vemos que en el constructor se añade a un map a nivel de clase un objeto que contiene el objeto broadcast y un set que contendrá las funciones que se reciban por el método onmessage. De esta forma, cuando se lance el escuchador del evento se itera cada uno de las funciones y se ejecutarán.


Con esto concluimos este artículo en el que hemos visto otra forma de crear una comunicación no en cascada entre elementos.