iframes – der heilige Gral bei verteilten Webanwendungen
Seite 4: 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.
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);
}
}