Blog

JavaScript Days | Angular Days | React Days | HTML & CSS Days
Die großen Trainingsevents für JavaScript, Angular, React, HTML & CSS
25
Jan

React goes PWA

PWA App augegliedert

Progressive Web Apps bieten dem Nutzer einer Anwendung zahlreiche interessante Vorteile. Um eine solche PWA mit React zu entwickeln, muss man jedoch ein wenig anders vorgehen als bei einer „normalen“ React-Anwendung. Mit den richtigen Hilfsmitteln ist das aber gar nicht so schwer.

Eine Progressive Web App (PWA) ist eine Webapplikation, die auf nahezu jedem Gerät läuft, installierbar und offlinefähig ist. Wirklich neu ist das Konzept der PWAs nicht, seit gut sechs Jahren stehen die Basistechnologien zur Verfügung und die Idee ist beschrieben. Leider nutzen immer noch viel zu wenige Entwickler die Stärken von PWAs. Ein guter Grund für uns, erneut einen Blick auf den Entwicklungsprozess und die Unterstützung von PWAs zu werfen. Unser Ziel ist es, mit möglichst geringem Aufwand eine vollwertige PWA aus dem Hut zu zaubern. Dafür bedienen wir uns einer Reihe von Bibliotheken und anderer Hilfsmittel. Unser Beispiel wird auf React basieren. Das heißt aber nicht, dass es nicht möglich wäre, eine solche PWA auch in Angular oder Vue zu implementieren, ganz im Gegenteil. Die meisten Dinge, die ich Ihnen hier zeige, gelten in ganz ähnlicher Form für alle großen Frontend-Frameworks.

In diesem Artikel bauen wir gemeinsam eine PWA, die sich installieren lässt, offline verwendbar ist und auf jedem Gerät funktioniert. Thematisch verabschieden wir uns von der obligatorischen To-do-Liste und setzen einen einfachen Einkaufszettel um. Bevor wir mit der ersten Zeile Quellcode beginnen, sollten wir ein paar grundsätzliche Fragen klären: Was ist eine PWA? Welche Anforderungen stellen wir an zeitgemäßen Quellcode und worum geht es in unserem kleinen Beispielprojekt? Die erste Frage beantwortet Google mit der PWA Checkliste unter [1] für uns:

  • Starts fast, stays fast: Die PWA soll möglichst schnell für die Benutzer verfügbar sein, doch hier endet die Reise nicht. Die Applikation soll sich während ihrer gesamten Laufzeit schnell anfühlen, am besten so, als gäbe es die Kommunikation zwischen Client und Server gar nicht und als müsste der Browser zum Wechseln der Ansicht nicht jede Menge Arbeit verrichten. Beim Thema Geschwindigkeit ist die Trickkiste von React sehr groß. Es beginnt bei Themen, die wir uns im Zuge dieses Artikels ansehen, wie beispielsweise das Speichern der gesamten Applikation im Service Worker. Darüber hinaus gibt es mit Server-side Rendering und Lazy Loading noch einige weitere Möglichkeiten, um die Applikation spürbar zu beschleunigen.

  • Works in any browser: Die unterschiedlichen Umgebungen, in denen Webapplikationen ausgeführt werden, sind seit jeher ein Problem für Webentwickler. Die Entwicklung von Werkzeugen wie Babel hat entscheidend dazu beigetragen, dass sich Entwickler nicht mehr so viele Gedanken über die Umgebung machen müssen, da sich verschiedene Browser- und Sprachfeatures mit Polyfills emulieren lassen. Generell hat das früher etablierte Browser-Sniffing ausgedient, bei dem die Entwickler anhand einer Kombination von Browser und seiner Versionsnummer entschieden haben, was die jeweiligen Benutzer zu sehen bekommen. An die Stelle dieser Praxis ist die sogenannte Feature-Detection getreten, bei der Sie als Entwickler nur noch prüfen, ob ein bestimmtes Feature, das Sie nutzen möchten, vorhanden ist oder nicht. Sie können dann entscheiden, ob Sie das Feature der Applikation im speziellen Fall komplett weglassen oder durch eine Alternative ersetzen.

  • Responsive to any screen size: Soll die Applikation in jedem Browser funktionieren, muss sie sich auch an die Bedingungen der jeweiligen Umgebung anpassen und sowohl in Chrome auf einem Desktopsystem funktionieren als auch in Safari auf einem iPhone bedienbar sein. Die Anforderung greift hier etwas zu kurz, denn die Applikation soll sich nicht nur der Bildschirmgröße anpassen, sondern für die Umgebung optimiert sein. Auf einem Desktopsystem mit Maus und Tastatur interagieren Benutzer ganz anders als etwa auf einem Tablet oder Smartphone. Beispiele hierfür sind, dass Smartphonenutzer eher das untere Screendrittel für Eingaben nutzen und Desktopnutzer in ihrer Interaktion so gut wie nicht eingeschränkt sind, und dass die Eingabe größerer Textblöcke eher auf Desktopsystemen und nicht auf Smartphones erfolgt.

  • Provides a custom offline page: Chrome hat die Abwesenheit einer Internetverbindung elegant gelöst, indem die Entwickler eine Offlineseite mit einem kleinen Spiel implementiert haben, bei dem Sie mit einem Dinosaurier über Hindernisse hüpfen können. In einer gut umgesetzten PWA werden Ihre Benutzer diese Seite niemals zu Gesicht bekommen, da Sie dafür sorgen, dass Ihre Applikation zu jedem Zeitpunkt eine sinnvolle Ausgabe erzeugt. Das gilt vor allem auch dann, wenn die Benutzer der Applikation keine Internetverbindung haben. Was in diesem Fall sinnvoll ist, unterscheidet sich von Applikation zu Applikation. Im einfachsten Fall sehen die Benutzer die wichtigsten statischen Informationen aus einem Cache. Offlinefähigkeit kann jedoch auch bedeuten, dass die Applikation vollständig einsetzbar ist, egal ob eine Verbindung besteht oder nicht.

  • Is installable: Eine auf einem Gerät installierte PWA verhält sich nicht mehr wie eine normale Webapplikation, sondern eher wie eine in der jeweiligen Umgebung native Applikation. Die Grundlage für die Installierbarkeit liefert der Browser. Um die Details müssen Sie sich als Entwickler jedoch selbst kümmern. Themen wie beispielsweise das Fragen nach einer Installation oder die bereits erwähnte Offlinefähigkeit, die in den meisten Fällen mit der Installierbarkeit einhergeht, müssen Sie selbst in die Hand nehmen.

Das Grundgerüst einer PWA

Den technischen Kern einer PWA bilden die Manifest-Datei und der Service Worker. Das Manifest beschreibt, wie sich die Applikation verhalten soll, wenn sie auf einem Gerät installiert wird. Der Service Worker ist ein Prozess im Browser, der verschiedene Aufgaben wie beispielsweise Caching, Push Notifications oder Background Sync übernehmen kann. Im einfachsten Fall nutzen Sie für diese beiden Bestandteile Ihrer PWA eine bestehende Vorlage und passen sie an Ihre Bedürfnisse an. React nimmt Ihnen mit Create React App (CRA) und dem PWA-Template an dieser Stelle einige Arbeit ab, indem es Ihnen sowohl eine Applikation als auch die notwendigen Strukturen für eine PWA erzeugt. Das Kommando npx create-react-app shopping-list –template cra-template-pwa-typescript erzeugt eine grundlegende PWA für Sie.

Wie schon erwähnt, bereitet CRA die Strukturen bereits so weit vor, dass Sie unmittelbar mit der eigentlichen Arbeit an Ihrer PWA beginnen können. Die ersten Schritte bestehen somit aus der Anpassung der vorbereiteten Strukturen.

Die Manifest-Datei

In der Manifest-Datei wird das Look and Feel der Applikation auf dem Zielsystem festgelegt. Sie liegt im public-Verzeichnis Ihrer Applikation und trägt den Namen manifest.json. Die Manifest-Datei wird mit dem link-Tag in der index.html-Datei der Applikation eingebunden, sodass sie vom Browser geladen und ausgewertet wird. Üblicherweise erzeugen Sie eine solche Manifest-Datei nicht selbst, sondern nutzen Hilfsmittel, die Ihnen bei der Generierung helfen, was auch CRA tut. Die Datei enthält den Kurz- und Langnamen der Applikation, einen Satz von Icons, den Start-URL, verschiedene Farbkonfigurationen sowie die display-Eigenschaft, die angibt, wie der Browser die installierte PWA anzeigen soll. Der hier vorausgewählte Wert „standalone“ sorgt dafür, dass die Kontrollelemente des Browsers ausgeblendet werden und die PWA wie eine native Applikation wirkt. An dieser Stelle sollten Sie die beiden Namen und die Icons austauschen. Die „theme_color“-Eigenschaft bestimmt, in welcher Farbe das Betriebssystem die Applikation darstellt. Die „background_color“ definiert die Hintergrundfarbe, die verwendet wird, bevor das Stylesheet geladen ist. Listing 1 enthält die angepasste manifest.json-Datei.

Listing 1: Manifest-Datei für die PWA

{
  "short_name": "Einkaufsliste",
  "name": "Offlinefähige Einkaufsliste",
  "icons": [
    {
      "src": "favicon.ico",
      "sizes": "64x64 32x32 24x24 16x16",
      "type": "image/x-icon"
    },{
      "src": "logo192.png",
      "type": "image/png",
      "sizes": "192x192"
    },{
      "src": "logo512.png",
      "type": "image/png",
      "sizes": "512x512"
    }
  ],
  "start_url": ".",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff"
}

Wenn Sie die Manifest-Datei anpassen, vergessen Sie das Austauschen der Icons nicht, da ansonsten bei der Installation die Standard-React-Icons benutzt werden. Konkret geht es um die Dateien favicon.ico, logo192.png und logo512.png. Nachdem Sie diese Anpassungen vorgenommen haben, sind Sie nur noch einen Schritt von einer funktionierenden PWA entfernt: Sie müssen noch den Service Worker aktivieren.

Service Worker

Der Service Worker ist der geheime Star einer PWA. Hierbei handelt es sich um einen Hintergrundprozess, von dem die Benutzer einer Applikation in der Regel nichts direkt mitbekommen. Der Service Worker ist eine Art Proxy-Prozess zwischen der JavaScript-Applikation im Browser und dem Webserver. Er kann über entsprechende Event Handler alle Anfragen, die den Browser verlassen, abfangen und sie entweder zum Server durchlassen oder direkt beantworten, sodass die Kommunikation den Browser nicht verlässt.

Für den Umgang mit Service Workern gibt es mit Workbox quasi eine Standardbibliothek. Sie stellt eine Plug-in-Infrastruktur zur Verfügung, mit der Sie verschiedene Aufgaben im Service-Worker-Kontext mit vorgefertigten Lösungsbausteinen schnell erledigen können. Ein typisches Beispiel hierfür ist das Cachen statischer Anfragen. Zu diesem Zweck bietet Workbox verschiedene Cache-Strategie-Plug-ins an. Ihr Funktionsumfang reicht von einem Cache-First-Ansatz, bei dem zunächst geprüft wird, ob sich eine Ressource bereits im Cache befindet, bis zum Network-First-Ansatz, wo zunächst am Webserver angefragt wird und erst, wenn er nicht erreichbar ist, der lokale Cache geprüft wird.

Das Service-Worker-Set-up besteht aus insgesamt zwei Teilen: der Registrierung des Service Workers in der Datei serviceWorkerRegistration.ts und dem eigentlichen Quellcode des Service Workers in der Datei service-worker.ts. Die zentrale Einstiegsdatei der Applikation, die index.tsx-Datei im src-Verzeichnis, stößt die Registrierung des Service Workers an. An dieser Stelle gibt es jedoch zwei Dinge zu beachten: Standardmäßig ist der Service Worker deaktiviert und muss von Ihnen explizit aktiviert werden, indem Sie statt der unregister– die register-Methode der Service-Worker-Registrierung aufrufen. Der zweite Punkt, den Sie beachten sollten, ist, dass der Service Worker nur für den Production Build der Applikation aktiviert wird. Der Grund

ist, dass der Service Worker den Code der Applikation in seinem Cache vorhält, was einer schnellen Rückmeldung im Browser bei Codeänderungen durch den Dev Server widerspricht. Testen lässt sich der aktuelle Stand, indem Sie die index.tsx-Datei entsprechend anpassen und dann mit dem Kommando npm run build einen Production Build anstoßen. Die gebaute Applikation liegt dann im build-Verzeichnis, das Sie über einen statischen Webserver ausliefern lassen können. Zum Test erreichen Sie das mit dem Kommando npx http-server build. Es sorgt dafür, dass mit Hilfe des http-server-Pakets ein lokaler Server hochgefahren wird, der die Applikation standardmäßig auf Port 8080 ausliefert. Mit der Standardapplikation, die CRA für Sie vorbereitet, sehen Sie den Inhalt der App-Komponente, ein sich drehendes React-Logo und etwas Text.

Die wirklich interessanten Aspekte der Applikation verbergen sich unter der Haube. Um mehr darüber zu erfahren, öffnen Sie die Entwicklerwerkzeuge Ihres Browsers und wechseln im Fall von Chrome in den Application-Tab. Hier sehen Sie alle für Ihre Applikation relevanten Informationen wie beispielsweise die Informationen des Manifests oder den Status des Service Workers, aber auch den Browserspeicher. Im Manifest können Sie überprüfen, ob alle Ihre Einstellungen korrekt übernommen wurden, und beim Service Worker sollten Sie sehen, dass ein Worker-Prozess den Status „activated and is running“ hat (Abb. 1). Über verschiedene Kontrollelemente können Sie den Service Worker anhalten oder aktualisieren. Außerdem können Sie Pushmitteilungen oder Sync-Anfragen auslösen.

springer_react_1.tif_fmt1.jpgAbb. 1: Informationen über den Service Worker in den Entwicklerwerkzeugen

Ein weiteres Werkzeug, das bei der Entwicklung von PWAs relevant ist, ist das Analysewerkzeug Lighthouse, das in die Chrome-Entwicklerwerkzeuge integriert ist. Um es zu verwenden, wechseln Sie auf den Lighthouse-Tab. Hier haben Sie die Möglichkeit, verschiedene Analysen auszuführen. Für uns ist momentan nur der Punkt Progressive Web App interessant. Deaktivieren Sie alle anderen Checkboxen und klicken auf den Button Generate report, überprüft Lighthouse die PWA auf die wichtigsten Best Practices (Abb. 2).

springer_react_2.tif_fmt1.jpgAbb. 2: Analyse der PWA mit Lighthouse

Wie Sie in der Auswertung sehen können, ist die Standardkonfiguration unserer React-Applikation schon ganz in Ordnung. Zu jedem Aspekt finden Sie in den Details weiterführende Informationen über die jeweilige Überprüfung und wie Sie Ihre Applikation an der jeweiligen Stelle verbessern können. Nun, da das grundlegende Set-up steht, können wir uns im nächsten Schritt der eigentlichen Applikation zuwenden.

Responsiveness

Die Einkaufslistenapplikation besteht aus einer Liste von Artikeln, die wir beim Einkaufen nicht vergessen sollten. Zum Hinzufügen eines neuen Artikels kann ein einfaches Formular verwendet werden, das am unteren Ende der Liste angezeigt wird. Jede Zeile der Liste kann durch das Betätigen eines Buttons als „habe ich schon in meinem Einkaufswagen“ gekennzeichnet werden. Über einen löschen-Button kann der jeweilige Artikel von der Liste entfernt werden. Sollten Sie sich bei einem Eintrag vertippt haben oder möchten Sie aus einem anderen Grund nachträglich Änderungen vornehmen, können Sie das tun, indem Sie auf den Namen des Artikels klicken. In diesem Fall wird ein Inline-Formular eingeblendet, über das Sie die Anpassungen vornehmen können. Auf die Implementierung der jeweiligen Komponenten gehen wir an dieser Stelle nicht genauer ein, da das nur wenig mit der Umsetzung der PWA zu tun hat. Abbildung 3 zeigt den initialen Stand der Applikation.

springer_react_3.tif_fmt1.jpgAbb. 3: Initiale Einkaufsliste

In der initialen Version sieht die Applikation noch wenig ansprechend und schon gar nicht responsiv aus. Für das Styling Ihrer Applikation haben Sie mehrere Möglichkeiten. Sie können sich komplett selbst um das Styling kümmern und mit CSS-Media-Queries auf die verschiedenen Screengrößen reagieren. Alternativ dazu ist es möglich, leichtgewichtige CSS-Lösungen wie beispielsweise Tailwind zu verwenden, das Ihnen auch eine Lösung für Responsive Design bietet. Die schnellste und einfachste Variante besteht jedoch darin, Bibliotheken wie Material-UI oder Bootstrap zu verwenden. Mit solchen Lösungen können Sie sehr schnell optisch ansprechende Oberflächen erzeugen, da Sie auf eine Vielzahl vorgefertigter Komponenten zurückgreifen können. Für unsere Implementierung bedeutet das ein paar Umbauten. So soll bei größeren Screens die Liste in der Mitte erscheinen und links und rechts davon freier Platz sein. Das Layout der Tabelle soll angepasst werden und auch die Buttons sollen ansprechender gestaltet werden. In Abbildung 4 sehen Sie die mit Material-UI versehenen Komponenten auf einem Smartphone.

springer_react_4.tif_fmt1.jpgAbb. 4: Mobile Ansicht der Einkaufsliste

Material-UI ist eine der am häufigsten in React-Applikationen verwendeten UI-Bibliotheken und bietet für so ziemlich alle Elemente, die wir bisher in der Applikation verwendet haben, eine passende Komponente. Das wichtigste Feature, um die Applikation für die Verwendung auf verschiedenen Geräten vorzubereiten, ist das Grid-System von Material-UI. Die Bibliothek arbeitet hier mit fünf vordefinierten Breakpoints, die jeweils einer Spanne von Fensterbreiten zugeordnet sind. In unserer Applikation sollen ab dem md-Breakpoint links und rechts zusätzliche Spalten angezeigt werden. In den kleineren Breakpoints wie beispielsweise auf dem Smartphone nimmt die Tabelle die gesamte Fensterbreite ein.

Installation der Applikation

Eine wesentliche Anforderung an eine PWA ist, dass sie installiert werden kann. Der Vorteil ist, dass die Installation komplett optional ist. Generell gilt ja für eine PWA, dass sie überall funktionieren soll und durch die Features der Umgebung angereichert wird. Grundlegendes HTML, CSS und JavaScript funktioniert überall.

Die Installation einer PWA können Benutzer zu jedem Zeitpunkt manuell anstoßen. Zu diesem Zweck gibt es ein kleines Symbol in der Adressleiste. Alternativ können Benutzer über das Menü des Browsers die Installation anstoßen. In einer PWA können Sie sich als Entwickler auch dazu entscheiden, Ihren Benutzern die Möglichkeit zu geben, die Installation aktiv aus der Applikation heraus zu starten. Der Vorteil dieser Variante ist, dass die Installierbarkeit den Benutzern viel deutlicher gemacht wird. Der Kern dieses Features ist ein Event Listener auf das beforeinstallprompt-Event, das mit Hilfe des Ref-Hooks zwischengespeichert wird. Den Benutzern der Applikation wird, sobald das Event ausgelöst wurde, also die Installation möglich ist, ein Button angezeigt. Klicken die Benutzer auf diesen Button, wird ein Dialog des Browsers angezeigt, mit dem die Installation bestätigt werden muss.

Offlinefähigkeit

Mit dem aktuellen Stand der Applikation kommen die Benutzer schon in den Genuss einiger Vorteile von PWAs wie das Caching des Service Workers, das für einen erheblichen Geschwindigkeitsvorteil beim Laden sorgt, und der Installierbarkeit der Applikation, die unsere Anwendung vom Browser entkoppelt. Ein wichtiger Baustein fehlt aber noch, damit sich unsere Einkaufsliste mit einer nativen mobilen oder Desktopapplikation messen kann: richtige Offlinefähigkeit. Grundsätzlich bedeutet das, dass eine Applikation auch ohne eine bestehende Internetverbindung weiter funktioniert. Für eine einfache Applikation lässt sich diese Anforderung noch gut mit den Bordmitteln des Service Workers erledigen. Kommt es jedoch darauf an, Daten vom Client aus zu erzeugen, zu manipulieren oder zu löschen, geraten wir mit unseren bisherigen Werkzeugen schnell an die Grenze des Möglichen. Die Lösung dieses Problems liegt in der Verwendung der Speicherschnittstelle des Browsers. Diese Schnittstelle umfasst sowohl Local- und Session-Storage als auch die IndexedDB. Die ersten beiden Varianten stellen einfache Key-Value-Stores zum Speichern von Informationen dar, haben aber den Nachteil, dass sie aus Worker-Prozessen wie dem Service Worker nicht verwendet werden können. Die IndexedDB ist eine deutlich mächtigere Lösung. Durch den namensgebenden Index werden lesende Zugriffe auf eine indexierte Eigenschaft der Daten deutlich schneller. Außerdem können Sie die IndexedDB im Hauptprozess, aber auch im Service Worker verwenden. Wie nahezu jedes Browser-API hat auch IndexedDB einen kleinen Haken: Die Schnittstelle ist nicht gerade ein Musterbeispiel guter Benutzbarkeit. Sie müssen sich um die Struktur, Versionierung und Asynchronität bei der Entwicklung selbst kümmern. Aus diesem Grund gibt es zahlreiche Abstraktionsbibliotheken, wie beispielsweise Dexie.js, die der IndexedDB den Schrecken nehmen, indem die Schnittstelle in Promises gewrappt und damit deutlich vereinfacht wird.

Doch zurück zu unserem eigentlichen Thema und einigen architektonischen Fragen, denen wir uns zunächst stellen müssen. Erst einmal sollten wir klären, was eigentlich der Begriff „offline“ bedeutet. Auf der einen Seite ist klar, dass die Abwesenheit einer Netzwerkverbindung, weil Sie sich beispielsweise gerade im tiefsten Wald ohne ausreichende Mobilfunkabdeckung befinden, ein klassisches Beispiel für offline ist. Aber was ist, wenn Sie mit einem WLAN innerhalb eines Unternehmens verbunden sind, die Firewall aber keinerlei Verbindung mit dem Internet zulässt? Technisch gesehen haben Sie zwar eine bestehende Verbindung, den Webserver der Applikation erreichen Sie aber dennoch nicht. Außerdem sollten Sie den unwahrscheinlichen Fall betrachten, dass Ihr Backend wegen eines Ausfalls generell nicht erreichbar ist, auch dann ist die Frontend-Applikation offline. Sie sehen, sogar bei einem so klaren Begriff wie „offline“ gibt es nicht nur schwarz und weiß, sondern einige Stufen dazwischen.

Dann haben Sie bei der Implementierung Ihrer Applikation die Qual der Wahl, ob Sie die Offlinefähigkeit im Service Worker oder direkt in Ihrer Applikation umsetzen möchten. Offlinefähigkeit im Service Worker bedeutet, dass Ihre Applikation von einer unterbrochenen Verbindung nicht das Geringste wissen muss. Der Nachteil dieser Methode ist jedoch, dass Sie eine zweite Applikation im Service Worker implementieren müssen, die sich um das lokale Speichern und die Serverkommunikation kümmern muss. Die zweite Variante, also die Offlinefähigkeit in der Applikation, bedeutet, dass Sie in der Applikation alle erforderlichen Strukturen für den Online- und Offlinebetrieb schaffen. Das heißt, dass die Applikation zwar komplexer wird, die Strukturen jedoch beisammen liegen, was beispielsweise das Testen der Applikation deutlich einfacher macht.

Jetzt ist die Offlinefähigkeit einer Webapplikation kein wirklich neues Thema und wir haben uns ja auf die Fahne geschrieben, möglichst wenig selbst zu schreiben. Das heißt, dass für unsere Applikation beide Ansätze wegfallen, da sie bedeuten, dass Sie die gesamte Logik für den Offlinebetrieb der Applikation selbst entwickeln müssten. Also werfen wir einen Blick auf die möglichen Alternativen. Und hier sticht eine Bibliothek heraus: PouchDB. Vielleicht haben Sie von dieser clientseitigen Datenbank, die in JavaScript implementiert ist, schon einmal gehört. Denn wirklich neu ist die Lösung nicht. PouchDB existiert seit 2012 und verfolgt den gleichen Ansatz wie die serverseitige CouchDB. CouchDB ist eine dokumentenorientierte Datenbank, die HTTP als Schnittstellenprotokoll verwendet und ein spezielles Replikationsprotokoll beinhaltet, um die verschiedenen CouchDB-Knoten eines Clusters zu synchronisieren. PouchDB greift die Ideen von CouchDB auf und bringt die Datenbank in den Browser und Node.js. Sie können PouchDB als Bibliothek betrachten, die die Datenhaltung in Ihrer Applikation komplett übernimmt – sie ist quasi ein Rundum-sorglos-Paket. Die Datenhaltung endet dabei nicht im Browser, sondern umfasst auch die Kommunikation mit einem Webserver und noch viel wichtiger die Synchronisierung der Daten zwischen Client und Server.

Integration in die Applikation

Bevor Sie mit der eigentlichen Arbeit mit PouchDB beginnen können, müssen Sie das Paket zunächst mit dem Kommando npm install pouchdb installieren. Da PouchDB keine eigenen Typdefinitionen für TypeScript mitbringt, installieren Sie diese mit npm install –D @ types/pouchdb. Mit diesem Set-up können Sie nun PouchDB in Ihrer Applikation nutzen. PouchDB ist unabhängig vom verwendeten Frontend-Framework, Sie können sie also neben React auch guten Gewissens in eine Angular- oder Vue-Applikation integrieren. Der erste Schritt besteht aus dem Aufbau der Verbindung zur Datenbank. Die PouchDB-Klasse ist als Generic-Typ implementiert, sodass Sie bei der Instanziierung den passenden Typ übergeben können. Mit der PouchDB-Instanz können Sie auf die lokale Datenbank zugreifen. Diese wird in der IndexedDB des Browsers abgelegt. Für die Verknüpfung mit einem CouchDB-Server müssen Sie nur noch die sync-Funktion aufrufen, der Sie die Adresse des Zielservers übergeben. Das zweite Argument ist ein Konfigurationsobjekt, das die Aspekte der Serververbindung steuert. Mit der live-Eigenschaft und dem Wert true sorgen Sie dafür, dass Client und Server kontinuierlich synchronisiert werden, also jede Änderung auf die Gegenseite übertragen wird (Listing 2).

Listing 2: Basis-Set-up PouchDB

import PouchDB from "pouchdb";
import { BaseItem } from './Item';
const db = new PouchDB<BaseItem>("shopping-list");
export const changes = db.changes({since: 'now', live: true});
db.sync('http://localhost:5984/shopping-list', { live: true });

Jetzt ist es an der Zeit, die Datenhaltung in unsere Applikation zu integrieren. Die Kommunikation mit der Datenbank ist komplett unabhängig von React, kann also in einer separaten Datei vorgehalten werden. Dieser Service exportiert Schnittstellen zum Auslesen aller Daten sowie zum Speichern und Löschen einzelner Datensätze. Außerdem exportieren Sie das Ergebnis der changes-Methode der Datenbank. Hierbei handelt es sich um einen EventEmitter, der bei allen Änderungen ein change-Event auslöst. Die Konfiguration der changes-Methode mit since und live sorgt dafür, dass für alle Änderungen ab dem aktuellen Zeitpunkt bis zu dem Zeitpunkt, zu dem der EventEmitter mit der cancel-Methode gestoppt wird, Events ausgelöst werden.

Die drei Funktionen savegetAll und remove in Listing 3 sind lediglich leichtgewichtige Wrapper. Die save-Funktion kümmert sich um den korrekten Aufruf der post- beziehungsweise put-Methode, um neue Datensätze anzulegen oder bestehende zu aktualisieren. getAll ruft die allDocs-Methode zum Auslesen aller Datensätze auf und sorgt dafür, dass das Resultat keine undefined-Werte enthält. Die remove-Funktion reicht schließlich den Aufruf an die remove-Methode zum Löschen von Datensätzen weiter.

Listing 3: list.service.ts

export function save(item: InputItem): Promise<Item> {
  if (item._id) {
    return update(item as Item);
  } else {
    return insert(item);
  }
}
 
async function update(item: Item): Promise<Item> {
  const { id: _id, rev: _rev } = await db.put(item);
  return { ...item, _id, _rev };
}
 
async function insert(item: InputItem): Promise<Item> {
  const { id: _id, rev: _rev } = await db.post(item);
  return { ...item, _id, _rev };
}
 
export async function getAll(): Promise<Item[]> {
  return (await db.allDocs({ include_docs: true, descending: true })).rows
    .map((row) => row.doc)
    .filter((doc) => doc !== undefined) as unknown as Item[];
}
 
export async function remove(item: Item): Promise<void> {
  db.remove(item);
}

Diese Schnittstellen nutzen Sie im nächsten Schritt in einem Custom Hook, wie Sie ihn in Listing 4 sehen, um die Daten der PouchDB mit dem State Ihrer React-Applikation zu verknüpfen.

Listing 4: Custom Hook für die State- und Änderungsverwaltung

import { useState, useEffect } from 'react';
import Item from './Item';
import { getAll, changes } from './list.service';
 
export function useList() {
  const [items, setItems] = useState<Item[]>([]);
  useEffect(() => {
    getAll().then((data) => setItems(data));
 
    function handleChange() {
      getAll().then((data) => setItems(data));
    }
 
    changes.on('change', handleChange);
 
    return () => {changes.removeListener('change', handleChange)};
  }, []);
 
  return items;
}

Der useList-Hook besteht aus einem State, in dem Sie die einzelnen Datensätze der Einkaufsliste vorhalten. Der useEffect-Hook sorgt dafür, dass die Daten initial durch den Aufruf der getAll-Funktion des Service geladen werden. Außerdem registrieren Sie die Callback-Funktion handleChange auf das change-Event der PouchDB. Theoretisch erhalten Sie die Änderungen an der Datenbank über das Event-Objekt der handleChange-Funktion. Allerdings müssen Sie in diesem Fall die verschiedenen Event-Typen, also beispielsweise Aktualisierungen und Löschungen, separat behandeln und den State entsprechend manipulieren. Die einfachere Lösung besteht darin, dass Sie bei einer Änderung den Datenstand der lokalen PouchDB erneut auslesen und den State des Custom Hooks einfach überschreiben. Diese Variante ist für Datenbanken mit größeren Datenmengen nicht geeignet. In diesem Fall müssen Sie sich dann selbst wieder um die Aktualisierung des States kümmern. Da auch lokale Änderungen im Offlinezustand solche Change-Events hervorrufen, funktioniert die Aktualisierung auch, wenn der Browser keine Verbindung zum Server hat.

Bei der Arbeit mit CouchDB gibt es ein paar Fallstricke, die Sie beachten sollten. Ohne weitere Konfiguration befindet sich eine CouchDB im sogenannten Admin-Party-Modus, bei dem die Datenbank völlig ungeschützt ist. Dieses Problem können Sie beheben, indem Sie einen Benutzernamen und ein Passwort vergeben. Das ist allerdings nur der erste Schritt, den Sie zur Absicherung Ihrer Schnittstelle machen sollten. CouchDB verfügt über ein Rollen- und Rechtekonzept, mit dem Sie die Zugriffe auf die Datenbank sehr genau steuern können. Auf Clientseite müssen Sie sich dann entsprechend anmelden, was über die Headerinformationen der Anfrage geschieht, die Sie bei der Instanziierung der Datenbank angeben können.

Nachdem der CouchDB-Server gerade während der Entwicklung oft unter einer anderen Adresse als die eigentliche Applikation läuft, kommt es hier häufig zu CORS-Problemen, sprich der Browser verbietet die Kommunikation zur CouchDB. Abhilfe schafft hier die Aktivierung der entsprechenden Header in der CouchDB. Die Konfiguration erreichen Sie bei einer lokalen CouchDB unter http://localhost:5984/_utils/#_config/[email protected]/cors. Hier können Sie CORS über einen Button aktivieren und sollten anschließend keine weiteren Probleme haben. Die grafische Oberfläche von CouchDB mit dem Namen Fauxton, die Sie unter http://localhost:5984/_utils erreichen, ist ein sehr hilfreiches Werkzeug bei der Entwicklung, mit dem Sie beispielsweise die Datensynchronisierung überprüfen können.

Fazit und Ausblick

Mit den hier beschriebenen Schritten verfügen Sie nun über eine vollwertige PWA, die sich installieren lässt, auf (nahezu) jedes Gerät passt und die obendrein auch noch offlinefähig ist. Unser erklärtes Ziel war, dass wir dieses Vorhaben mit so wenig Aufwand wie möglich erreichen. Also werfen wir kurz noch einen Blick zurück auf den Weg, den wir bis hier gegangen sind:

  • PWA-Grundlagen: Das Manifest sowie die grundlegende Service-Worker-Konfiguration wurden uns von Create React App zur Verfügung gestellt.

  • Responsiveness: Mit dem Einsatz von Material-UI ist die Umsetzung einer responsiven Applikation, die auf unterschiedliche Bildschirmgrößen reagiert, beispielsweise durch das Grid-System möglich.

  • Installierbarkeit: Die grundlegende Installierbarkeit ist bereits durch die vorgefertigte Konfiguration gegeben. Wir haben diese lediglich um die Möglichkeit erweitert, dass die Benutzer der Applikation die Installation aus der Applikation heraus anstoßen können.

  • Offlinefähigkeit: Mit PouchDB kennen Sie spätestens jetzt eine Bibliothek, die Ihnen die meiste Arbeit mit der Offlinefähigkeit einer Applikation abnimmt, und das beinhaltet sogar tiefergehende Themen wie die Synchronisierung der Daten mit dem Server.

An dieser Stelle ist die Reise zur perfekten PWA natürlich noch lange nicht zu Ende. Es gibt noch zahlreiche Browserschnittstellen, die Sie einbinden können, und viele Features, die Sie auf dieser Basis umsetzen können. Bei allen Erweiterungen sollten Sie sich jedoch stets fragen: Haben das nicht schon andere für mich gelöst und kann ich eine solche bestehende Lösung nicht für meine Applikation anpassen und verwenden?

Links & Literatur

[1] https://web.dev/pwa-checklist/

Immer auf dem Laufenden bleiben!
Alle News & Updates: