Multi-Tabs-Kommunikation über LocalStorage

07 November 2017
Alexander Baier

Ich habe vor Kurzem eine Anwendung Multi-Tab fähig gemacht. Einige der Anforderungen setzten voraus, dass der klientseitige Code in unterschiedlichen Tabs miteinander kommunizieren kann. Für die Kommunikation wurde eine Schnittstelle benötigt, über die Nachrichten ausgetauscht werden können. Im Folgenden möchte ich meinen Ansatz dazu vorstellen.

Die Lösung beruht auf dem Publish/Subscribe Modell zum austauschen von Nachrichten: Es gibt eine API an der ich eine Funktion registrieren (subscribe) kann, um nachfolgend veröffentlichte (publish) Nachrichten zu empfangen. Desweiteren bietet die API die Möglichkeit die Registration einer bestimmte Funktion wieder rückgängig zu machen. Wenn wir zunächst eine Implementierung betrachten, welche ausschließlich innerhalb eines Tabs funktioniert, würde diese zum Beispiel wie folgt aussehen.

my.namespace.messageBus = my.namespace.messageBus || (function () {
    var subscribers = {};
    var nextID = 1;

    return {
        subscribe: subscribe,
        unsubscribe: unsubscribe,
        publish: publishInternal
    };

    function subscribe(callback) {
        subscribers[nextID] = callback;
        var id = nextID;
        nextID++;
        return id;
    }

    function unsubscribe(id) {
        delete subscribers[id];
    }

    function publishInternal(msg) {
        Object.keys(subscribers).map(function (id) {
            subscribers[id](msg);
        });
    }
})();

Die registrierten Funktionen, auch "Subscribers" genannt, werden in einer Key-Value-Struktur abgelegt. Wird nun die publish Funktion aufgerufen, wird die übergebene Nachricht an alle Subscriber weitergegeben. Das Entfernen eines Subscribers geschieht über dessen ID, welche bei der Registrierung erzeugt und an den Aufrufer zurückgegeben wird.

Um eine veröffentlichte Nachricht nun auch an Subscriber in einem anderen Tab zu schicken, benötigen wir den LocalStorage. Wir machen uns das storage-Event zunutze, welches ausgelöst wird, sobald jemand etwas in den LocalStorage des Browsers schreibt. Das storage-Event enthält dann den Schlüssel über den der geschriebene Wert identifiziert wird, den alten und den neuen Wert. Das Senden einer Nachricht über den LocalStorage ist also nichts weiteres als das Schreiben und Entfernen der Nachricht unter einem bekannten Schlüssel:

function publishToStorage(msg) {
    window.localStorage.setItem(localStorageKey, JSON.stringify(msg));
    window.localStorage.removeItem(localStorageKey);
}

Damit die Nachricht in anderen Tabs auch ankommt, müssen wir noch einen EventListener an das storage-Event hängen:

function setupStorageEventHandlers() {
    window.addEventListener("storage", function (e) {
        if (e.key === localStorageKey && e.newValue !== null && e.newValue !== "") {
            var msg = JSON.parse(e.newValue);
            publishInternal(msg);
        }
    });
}

Ersetzten wir publishInternal noch durch publish in unserer API, haben wir unseren Tab-Kommunikation auch schon fertig:

function publish(msg) {
    publishToStorage(msg);
    publishInternal(msg);
}

Eine letzte Anmerkung bleibt noch: Wenn der LocalStorage nicht zur Verfügung steht, wie zum Beispiel in Safari im privaten Modus, funktioniert unsere tabübergreifende Kommunikation natürlich nicht. Aus diesem Grund prüft meine Lösung vor dem Zugriff auf den LocalStorage, ob dieser wirklich verfügbar ist. Sollte dies nicht der Fall sein, wird die Nachricht nur intern versendet.

function publish(msg) {
    if (isLocalStorageSupported() === true) {
        publishToStorage(msg);
    }

    publishInternal(msg);
}

function isLocalStorageSupported() {
    try {
        window.localStorage.setItem('storageTest', 'test');
        window.localStorage.removeItem('storageTest');

        return true;
    }
    catch (exc) {
        return false;
    }
}

Zusammenfassend, haben wir eine Lösung für das Problem der tabübergreifenden, klientseitigen Kommunikation gefunden und diese mit wenig Code implementiert.