iframes – der heilige Gral bei verteilten Webanwendungen

IFrameBridge – die Brücke zwischen eingebetteten Komponenten

Im Folgenden wird eine TypeScript-Implementierung zur einfachen Integration von iframes über eine Bridge-Komponente vorgestellt. Die sogenannte IFrameBridge übernimmt dabei die Kommunikation zwischen eingebetteten Inhalten und dem Root-Dokument (hier Shell). Sie kapselt somit die Details beim Registrieren, Nachrichtentransport und Event-Handling (vgl. Abbildung 3).

Um iframes zu registrieren, lässt sich der HTML-onload-Event verwenden. Er wird aufgerufen, wenn die Ressource und ihre abhängigen Komponenten geladen sind. Das heißt, eine Anmeldung des iframes an der Bridge erfolgt erst, wenn der Ladevorgang vollständig abgeschlossen ist. Das stellt sicher, dass Nachrichten nur an bereits fertig geladene Komponenten geschickt werden. Der load-Event lässt sich über das load-Attribut des HTML-iframe-Tags überschreiben.

Aufbau der IFrameBridge im Überblick (Abb. 3) (Bild: Volkswagen AG)

Beim Abschluss des Ladevorgangs lässt sich die registerIFrame()-Methode der Bridge aufrufen, wie im nachfolgenden Listing zu sehen. Alternativ ist es möglich, die Registrierung über einen zentralen Event-Handler in TypeScript zu realisieren, um den rein deklarativen Gedanken von HTML zu erhalten. Alle für die Registrierung notwendigen Informationen wie URL und ID des iframes liefert das Event-Objekt mit.

<iframe src="http://example.de"
id="iframe1"
style="border:none;"
width="800" height="600"
sandbox="allow-scripts"
onload="iFrameBridge.registerIFrame(event)">
</iframe>

Das nächste Listing zeigt die vollständige Implementierung der IFrameBridge in TypeScript. Um eine bessere Nachvollziehbarkeit zu gewährleisten, ist der Code vereinfacht dargestellt. Zugunsten der Lesbarkeit entfallen insbesondere einige Sicherheitsprüfungen. Die IFrameBridge speichert alle iframes einer Webanwendung innerhalb einer Map. Als Schlüssel dient hierfür die jeweilige URL des iframes.

Das iframe wird durch eine Klasse IFrame repräsentiert, die die wesentlichen Merkmale wie URL, ID und Referenz zum HTML-Dokument speichert. Beim Registrieren sind die URL und die ID aus dem LoadEvent zu verwenden (target.src und target.id), damit wird ein neues IFrame initialisiert und in der Map abgelegt. Im Konstruktor des IFrame lässt sich auf Basis der ID eine Referenz auf das Zielfenster über die JavaScript-Methode getElementById() erstellen.

export class IFrameBridge {
private iFrameMap = new Map<string, IFrame>();

public registerIFrame(event) {
const url = event.target.src;
if (isTrustedURL(url)) {
this.iFrameMap.set(url, new IFrame(url, event.target.id);
}
}

public postMessageToIFrame(url: string, message: any) {
if (this.iFrameMap.has(url)) {
this.iFrameMap.get(url).postMessage(message);
}
}

// ...
}

class IFrame {
private id: string;
private url: string;
private htmlIFrameWindow: Window;

constructor(url: string, id: string) {
this.url = url;
this.id = id;
this.htmlIFrameWindow =
(<HTMLIFrameElement>document.getElementById(this.id)).contentWindow;
}

public postMessage(message) {
this.htmlIFrameWindow.postMessage(message, this.url);
}

// ...
}

Um Nachrichten an iframes zu versenden, kann man die Methode postMessage der Bridge nutzen. Der Aufrufer muss sich an dieser Stelle keine Gedanken zur HTML-ID oder Referenz auf das Zieldokument machen. Der Aufruf erfordert lediglich die Parameter Zieladresse "URL" und Nachricht. Die Bridge gewährleistet, dass die Nachricht an das richtige iframe übermittelt wird. Um das aus einer Demo-Komponente heraus umzusetzen, wäre es beispielsweise möglich, die Methode postMessageToIFrame() durch eine Schaltfläche im UI zu triggern:

export class DemoComponent { 
constructor(public iFrameBridge: IFrameBridge) { ... }

public postMessageToIFrame() {
this.iFrameBridge.postMessageToIFrame( "http://example.de",
{"message": "Hello World"});
}
}

Die Erweiterung der Bridge ermöglicht das Handling von MessageEvents. Hierzu implementiert die IFrameBridge das EventListenerObject-Interface und lauscht auf alle eingehenden Message Events der Webanwendung. Die Aufgabe der Bridge ist es, eingehende Nachrichten an registrierte EventListener zu vermitteln. Das heißt, eine Komponente der Webanwendung kann sich als Empfänger für eine Nachrichtenquelle anmelden und wird über alle von ihr verschickten Nachrichten informiert.

Hierzu muss die Komponente das Interface IFrameEventListener mit der Methode onMessageFromIFrame() implementieren. Die Registrierung erfolgt über die Methode addEventListener() der Bridge. Die EventListener werden in einem Array der IFrame-Klasse gespeichert:

export interface IFrameEventListener {
onMessageFromIFrame(message: any);
}

export class IFrameBridge implements EventListenerObject {
private iFrameMap = new Map<string, IFrame>();

constructor() {
window.addEventListener('message', this);
}

public registerIFrame(event) { ... }

public postMessageToIFrame(url: string, message: any) { ... }

public addEventListener(url: string, eventListener: IFrameEventListener){
if (this.iFrameMap.has(url)) {
this.iFrameMap.get(url).addEventListener(eventListener);
}
}

public handleEvent(event) {
if (this.iFrameMap.has(event.origin)) {
this.iFrameMap.get(event.origin).notifyEventListeners(event.data);
}
}
}

class IFrame {
// ...
private eventListeners = new Array<IFrameEventListener>();

constructor(url: string, id: string) { ... }

public postMessage(message) { ... }

public addEventListener(eventListener: IFrameEventListener) {
this.eventListeners.push(eventListener);
}

public notifyEventListeners(data) {
for (const listener of this.eventListeners) {
listener.onMessageFromIFrame(data);
}
}
}

Empfängt die Bridge eine Nachricht, erfolgt der Aufruf ihrer handleEvent-Methode. Diese prüft, ob es zu der Event-Quelle (event.origin) ein registriertes IFrame gibt. Auf diesem lässt sich dann im nächsten Schritt die notifyEventListeners()-Methode aufrufen, die alle angemeldeten EventListener über die eingegangene Message benachrichtigt.

Als robustere Variante könnte die Benachrichtigung der EventListener auch asynchron erfolgen. Das Lauschen auf Nachrichten aus iframes ist auf diese Weise sehr einfach in Komponenten der Webanwendung zu integrieren. Hierzu ist lediglich das Interface IFrameEventListener zu implementieren und die Komponente als EventListener über iFrameBridge.addEventListener() zu registrieren:

export class DemoComponent implements IFrameEventListener { 
constructor(public iFrameBridge: IFrameBridge) {
this.iFrameBridge.addEventListener("http://example.de", this);
}

public postMessageToIFrame() { ... }

public onMessageFromIFrame(data) {
console.log(data.message);
}
}