BroadcastChannel API met Angular
SOFTWAREONTWIKKELINGCUSTOM SOFTWAREANGULAR
18/06/2020 • Davy Steegen

Hoe de BroadcastChannel API gebruiken met Angular

Heb je ooit gehoord van de BroadcastChannel API? Een paar weken geleden hadden we dat ook niet. We stuitten er toevallig op toen we op zoek waren naar een oplossing die ons in staat stelde te communiceren tussen verschillende browservensters van dezelfde oorsprong. In deze blogpost bespreken we de API zelf en leren we je hoe je de BroadcastChannel API kunt gebruiken binnen een Angular applicatie.

De BroadcastChannel API

Stel je voor dat je een webpagina hebt geopend in meerdere tabbladen, en je wilt communiceren tussen deze tabbladen om ze up-to-date te houden. Hoe zou je daar zelfs maar aan beginnen? Na wat speurwerk op het internet kwamen we de BroadcastChannel API tegen die rechtstreeks in webbrowsers is geïmplementeerd.

Het blijkt dat deze API al sinds 2015 beschikbaar is. Mozilla Firefox 38 was de eerste browser die de specificatie overnam. In de loop van de volgende jaren volgden andere browsers het voorbeeld van Mozilla.

Bekijk de demo hieronder om een voorbeeld te zien van wat met deze technologie mogelijk is.

Broadcast Channel demo

Hoewel het een zeer eenvoudige demo is, toont hij toch de ware kracht van de BroadcastChannel API. In dit voorbeeld wordt de teller synchroon gehouden tussen de twee vensters. Het is misschien niet je typische voorbeeld, maar je kunt de API gebruiken om:

  • een gebruiker uit te loggen van een toepassing die in meerdere browsertabbladen draait,
  • een winkelwagentje gesynchroniseerd houden in een ander browsertabblad,
  • en gegevens verversen in andere tabbladen.

De BroadcastChannel is in feite een event bus met een producent en een of meer consumenten van het event:

BroadcastChannel message

Hoe stel je een BroadcastChannel in?

Een BroadcastChannel maken

Het maken van een BroadcastChannel is heel eenvoudig. Er hoeven geen bibliotheken te worden geïmporteerd in je code. Je hoeft alleen maar de constructor aan te roepen met een String die de naam bevat van het kanaal dat moet worden aangemaakt.

const broadcastChannel = new BroadcastChannel('demo-broadcast-channel');

Een bericht verzenden

Nu we een kanaal hebben opgezet, kunnen we het gebruiken om berichten te posten. Het posten van een bericht kan worden gedaan door de postMessage op te roepen op het BroadcastChannel dat je eerder hebt gemaakt.

this.counter++;
 
broadcastChannel.postMessage({
  type: 'counter',
  counter: this.counter
  });
}

De postMessage kan allerlei soorten objecten als bericht aannemen. Je kunt in principe alles verzenden wat je wilt, zolang de consument maar weet hoe die met de ontvangende objecten moet omgaan. Het is echter een goede gewoonte om in je berichten een veld op te nemen dat beschrijft om welk soort bericht het gaat. Dit maakt het gemakkelijker om in te schrijven op berichten van een bepaald type in plaats van een BroadcastChannel per type bericht te hebben.

Een bericht ontvangen

Aan de consumentenkant moet je een BroadcastChannel aanmaken met dezelfde naam als aan de producentenkant. Als de namen niet overeenstemmen, zult je (uiteraard) geen berichten ontvangen. Vervolgens moet je de onmessage callback implementeren.

const broadcastChannel = new BroadcastChannel('demo-broadcast-channel');
 
this.broadcastChannel.onmessage = (message) => {
  console.log('Received message', message);
}

Het BroadcastChannel dat een bericht plaatst zal het bericht zelf niet ontvangen, zelfs niet als het een luisteraar heeft geregistreerd. Als je echter een aparte BroadcastChannel instantie maakt voor het posten en consumeren van berichten, zal het browservenster dat het bericht heeft gepost het bericht ontvangen. Waarschijnlijk is dat niet iets wat je wilt. Om dit te vermijden is het het beste om een singleton instantie per BroadcastChannel aan te maken.

Een herbruikbare BroadcastService maken

Je wilt niet overal in je code naar de BroadcastChannel API verwijzen waar je berichten moet produceren/consumeren. Laten we in plaats daarvan een herbruikbare service maken die de logica inkapselt. Op die manier hoef je, als je ooit de BroadcastChannel wilt vervangen door een andere API, maar één dienst te updaten.

import {Observable, Subject} from 'rxjs';
import {filter} from 'rxjs/operators';
 
interface BroadcastMessage {
  type: string;
  payload: any;
}
 
export class BroadcastService {
  private broadcastChannel: BroadcastChannel;
  private onMessage = new Subject<any>();
 
  constructor(broadcastChannelName: string) {
    this.broadcastChannel = new BroadcastChannel(broadcastChannelName);
    this.broadcastChannel.onmessage = (message) => this.onMessage.next(message.data);
  }
 
  publish(message: BroadcastMessage): void {
    this.broadcastChannel.postMessage(message);
  }
 
  messagesOfType(type: string): Observable<BroadcastMessage> {
    return this.onMessage.pipe(
      filter(message => message.type === type)
    );
  }
}

In deze specifieke dienst hebben we goed gebruik gemaakt van RxJS Observables. Let goed op de messagesOfType functie: in dit geval hebben we de standaard RxJS filter operator gebruikt om alleen de berichten terug te geven die overeenkomen met het opgegeven type. Mooi en eenvoudig!

De service is bijna klaar voor gebruik in je Angular applicatie. Er is nog maar één uitdaging die nog op je wacht.

Draaien binnen de Angular Zone

Als je Angular al een tijdje gebruikt, ken je waarschijnlijk de Angular Zone. Code die binnen de Angular Zone wordt uitgevoerd zal automatisch de wijzigingsdetectie activeren. 

De service hierboven draait niet de Angular Zone, omdat het een API gebruikt die niet aan Angular is gekoppeld. Als het een bericht ontvangt en de interne toestand van een component bijwerkt, is Angular zich daar niet onmiddellijk van bewust. Dat betekent dat je de wijzigingen niet onmiddellijk in de browser terugziet. Pas nadat de volgende wijzigingsdetectie is getriggerd, zullen de resultaten zichtbaar zijn in de browser.

Om dit probleem te omzeilen, kunt je een aangepaste RxJS OperatorFunction maken. Het enige doel van de OperatorFunction is ervoor te zorgen dat elke life cycle hook van een Observable binnen de Angular Zone wordt uitgevoerd.

import { Observable, OperatorFunction } from 'rxjs';
import { NgZone } from '@angular/core';
 
/**
 * Custom OperatorFunction that makes sure that all lifecycle hooks of an Observable
 * are running in the NgZone.
 */
export function runInZone<T>(zone: NgZone): OperatorFunction<T, T> {
  return (source) => {
    return new Observable(observer => {
      const onNext = (value: T) => zone.run(() => observer.next(value));
      const onError = (e: any) => zone.run(() => observer.error(e));
      const onComplete = () => zone.run(() => observer.complete());
      return source.subscribe(onNext, onError, onComplete);
    });
  };
}

NgZone is een door Angular verstrekt object dat je kunt gebruiken om programmatisch code uit te voeren binnen Angular's zone. Het enige dat nu nog rest is de bovenstaande OperatorFunction te gebruiken in onze BroadcastService.

...
import {runInZone} from './run-in-zone';
 
export class BroadcastService {
...
 
  constructor(broadcastChannelName: string, private ngZone: NgZone) {
    this.broadcastChannel = new BroadcastChannel(broadcastChannelName);
    this.broadcastChannel.onmessage = (message) => this.onMessage.next(message.data);
  }
...
  messagesOfType(type: string): Observable<BroadcastMessage> {
    return this.onMessage.pipe(
      // It is important that we are running in the NgZone. This will make sure that Angular component changes are immediately visible in the browser when they are updated after receiving messages.
      runInZone(this.ngZone),
      filter(message => message.type === type)
    );
  }
}

Na het bijwerken van de dienst zullen de wijzigingen onmiddellijk zichtbaar zijn bij het ontvangen van berichten.

Injecteren van de dienst

Je kunt Angular's InjectionToken gebruiken om een singleton instantie van de service te maken. Declareer de InjectionToken:

export const DEMO_BROADCAST_SERVICE_TOKEN = new InjectionToken<BroadcastService>('demoBroadcastService', {
  factory: () => {
    return new BroadcastService('demo-broadcast-channel');
  },
});

Injecteer de service via de InjectionToken:

constructor(@Inject(DEMO_BROADCAST_SERVICE_TOKEN) private broadcastService: BroadcastService) {
}

Wordt de BroadcastChannel API overal ondersteund?

Je moet het volgende in gedachten houden bij het gebruik van de BroadcastService. Het zal alleen werken wanneer

  • alle browservensters draaien op dezelfde host en poort,
  • alle browservensters hetzelfde schema gebruiken (het zal niet werken als één app met https wordt geopend en de andere met http),
  • de browservensters niet geopend zijn in incognitomodus,
  • je browservensters zijn geopend in dezelfde browser (er is geen cross-browser compatibiliteit).

Alle moderne browsers ondersteunen de BroadcastChannel API, met uitzondering van Safari en Internet Explorer 11 (en lager). Voor een volledige lijst van compatibele browsers, zie Caniuse.

Als je een soortgelijke oplossing moet implementeren in niet-ondersteunde browsers, kunt je in plaats daarvan de LocalStorage van de browser gebruiken.

Takeaway

In deze blogpost hebben we kort beschreven hoe je gebruik kunt maken van de browsers BroadcastChannel API binnen een Angular applicatie. We hebben ook gekeken naar een oplossing over hoe de API kan worden gekoppeld aan Angular's Zone. Je kunt de volledige code van de demo vinden op Stackblitz. Bovendien kun je de BroadcastChannel API documentatie raadplegen op MDN Web Docs.