Offline REST Fallback

Offline REST Fallback via LocalStorage

Von , und am 30.11.2013

Für unser Projektmanagement-Tool „Herculess“ ist ein wichtiger Aspekt, dass die App Offline-fähig ist. Während das in einer nativen App relativ einfach möglich ist, ist solch eine Funktionalität bei Web-Apps immer noch nur sehr selten zu finden. Die Web-App von Herculess basiert auf dem JavaScript-MVC-Framework Ember.js, welches, ergänzt durch Ember-Data, sehr einfach mit REST-APIs umgehen kann. So kümmert sich das Framework komplett um den Datenerhalt und die Synchronisierung und der Developer muss nur mit den von Ember bereitgestellten Objekten arbeiten. Auch Promises werden automatisch implementiert.

Während Ember-Data somit extrem hilfreich ist und auch für sehr komplexe Applikationen geeignet ist, stießen wir auf das Problem, dass man zwar einstellen kann, ob man die Daten über eine REST-API oder den LocalStorage persistieren möchte, aber nicht beide Optionen gleichzeitig nutzen kann. Für Herculess brauchen wir aber eine Funktionalität, die standardmäßig Daten vom REST-Server holt und nur im Offline-Fall auf zwischengespeicherte Daten aus dem LocalStorage zurückgreift.

Der Weg zum Ziel

Die Lösung dieses Problems war nicht ganz einfach und hat erst einmal zu diversen Fehlversuchen geführt. Erste Ansätze waren es, den bestehenden LocalStorage-Adapter von Ember-Data umzubauen und um eine Hintergrund-Synchronisierung zu ergänzen. So hätte Ember immer lokal im LocalStorage gearbeitet und nur im Hintergrund wären die Daten von der REST-API in den LocalStorage geschrieben worden. Das Problem hierbei war jedoch, dass Ember, um stets eine Datenintegrität zu gewährleisten, es nicht ermöglicht, zur Laufzeit alle Daten aus dem LocalStorage zu erneuern. Es hätte stets nach dem Update von der REST-Schnittstelle eine kompletter Seitenrefresh durchgeführt werden müssen, was wiederrum die unbemerkte Synchronisierung unmöglich gemacht hätte.

Nachdem diverse Experimente in diese Richtung fehlgeschlagen sind, haben wir einen neuen Ansatz ausprobiert: Wenn die App offline ist, sollen die Ajax-Calls einfach statt an den Server an eine (offline verfügbar gemachte) Datei geroutet werden und diese dann aus dem LocalStorage zwischengespeicherte Daten in derselben Form wie die richtige REST-API zurückgeben. So könnte der Client immer gleich arbeiten und nur die URL der AJAX-Calls müsste geändert werden. Das Problem hierbei ist natürlich, dass man lokal nur JavaScript/HTML zwischenspeichern kann und diese clientseitig keine reinen JSON-Responses zusammenbauen können: Die AJAX-Calls parsen die Responses ja nicht, sondern geben nur den Dateiinhalt zurück.

Somit sind wir schlussendlich bei einer Adaption dieser Idee gelandet. Anstatt eine Pseudo-API zu implementieren, fangen wir alle AJAX-Calls ab und liefern mithilfe des jQuery-Plugins jquery.ajax.fake selbst zusammengebaute JSON-Responses zurück.

Unsere Lösung

In der Praxis sieht das wie folgt aus:

Der erste Schritt ist es, die Daten überhaupt einmal zwischen zu speichern. Dafür haben wir eine JavaScript-Klasse erstellt, welche für alle Synchronisations-Vorgänge zuständig ist. Diese ist komplett unabhängig von Ember.

Diese Klasse hat eine „sync()“ Funktion. Diese Funktion holt sich von der REST-API über eine eigene Route „/downSync“ eine Kollektion aller wichtigen Daten für diesen User. Diese Kollektion enthält alle Tasks und Projekte, die für diesen User wichtig sind.

Das Sync-Objekt speichert diese Daten dann in den LocalStorage. Danach wird nach einigen Sekunden erneut sync() aufgerufen und die Daten im LocalStorage aktualisiert. Somit sind die Daten im LocalStorage immer (bis auf seltene Ausnahmen) eine Spiegelung der Daten von der REST-API.

Der nächste Schritt ist es, abzufragen, ob gerade eine Internet-Verbindung besteht. Das machen wir beim ersten Seitenaufruf über „navigator.onLine“, welcher den Online-Status zurückgibt. So wird ein Cookie „is-offline“ auf true oder false gesetzt. Danach wird bei jedem Sync-Versuch dieser Cookie angepasst, falls der Sync-Versuch fehlschlägt oder doch wieder klappt.

Falls die App erkennt, dass man plötzlich offline geht, schaltet sie auf jquery.ajax.fake um. Dafür verändern wir über die jQuery-Funktion „$.ajaxSetup“, welche Voreinstellung für alle folgenden $.ajax-Calls setzen kann, das Attribut „fake: true“. Dieses braucht jquery.ajax.fake, um zu wissen dass dieser Request gefakt werden soll.

Zusätzlich dazu muss man, damit die Fake-Responses funktionieren, für jede URL einstellen, welcher Response kommen soll. Da es unmöglich wäre, hier im Vorfeld alle möglichen URLs einzustellen (da wir ja auch Parameter wie IDs mitsenden), muss das automatisch geschehen. Das haben wir so gelöst, dass wir in der Ember-Data Methode, die für die AJAX-Aufrufe zuständig ist, noch eine Zeile hinzugefügt haben, die – vor dem Senden – zu der nun gleich gesendeten URL immer die gleiche Funktion aufrufen soll.

$.ajax.fake.registerWebservice(url, function(data) {
    return restSyncer.fakeAjax(data);
});

Außerdem fügen wir vor dem Senden die URL als Parameter zum AJAX-Request hinzu.

Die Funktion, welche die Fake-AJAX-Responses zusammen bauen soll, kann dann über den so mitgelieferten Parameter „url“ ermitteln, welche Route angesprochen wurde, und dann aus dem LocalStorage entsprechend einen Response zusammenbauen und zurückgeben. Die eigentliche App merkt so nie, ob die Daten jetzt wirklich vom Server oder aus dem LocalStorage kommen. Sobald die App wieder online ist, wird automatisch umgestellt – ohne, dass der User es merkt.

Während diese Methode, soweit bisherige Test gezeigt haben, sehr gut und nahtlos funktioniert, hat sie auch zwei Nachteile: Jede Änderung der REST-API muss genau im Client gespiegelt werden, und es entsteht zusätzlicher Traffic durch das Syncen im Hintergrund. Letzteres werden wir in späteren Versionen optimieren, indem wir einen Timestamp der letzten Synchronisierung mitschicken und so nur neue Einträge erhalten. Außerdem soll man in der App die Frequenz der Sync-Abfragen einstellen können oder auch nur manuell synchronisieren.

Die aktuelle Implementation funktioniert bereits für alle GET-Requests. POST, PUT oder DELETE Requests sind aber genauso lösbar: Hierfür werden wir alle solche offline getätigten Requests eigens im LocalStorage speichern, und wenn die App wieder online geht werden diese gespeicherten Requests einer nacheinander erneut an den Server gesendet. Hierbei ist es besonders wichtig, den Status mitzuspeichern: Erstens muss der User immer wissen, ob gerade etwas synchronisiert wird oder ob alle Daten aktuell sind, und zweitens muss die App immer wissen, ob ein Request erfolgreich beim Server angekommen ist oder ob es wieder einen Fehler gab und es später erneut versucht werden muss, um den Verlust von Daten zu verhindern.

The comments are closed.