Mit Web-Technologie zu universeller App-Kompatibilität, Teil 2 Das Innenleben einer PWA

Von Filipe P. Martins & Anna Kobylinska 9 min Lesedauer

Progressiven Web-Apps liegen moderne Webtechnologien und -Prinzipien zu Grunde. Mit diesem innovativen Ansatz gelangen Web-Developer in nur wenigen Schritten zu einer Universal-App. Gegenüber anderen Ansätzen ist das ein echter Fortschritt, wenn die PWA gut umgesetzt wird.

Ein Service-Worker in den Chrome DevTools.(Bild:  BMW / Google)
Ein Service-Worker in den Chrome DevTools.
(Bild: BMW / Google)

Die Einstellungen für eine PWA auf dem Endgerät verwaltet der voreingestellte Webbrowser.(Bild:  BMW / Google)
Die Einstellungen für eine PWA auf dem Endgerät verwaltet der voreingestellte Webbrowser.
(Bild: BMW / Google)

Eine PWA mag sich wie eine native App anfühlen, doch sie ist nichts anderes als eine Anwendung, die „im tiefen Inneren“ Webtechnologien nutzt und Daten zwischen einem Endgerät und einem Webserver austauscht. Sie mag ja offline funktionieren, Push-Benachrichtigungen senden, auf Gerätefunktionen zugreifen und sich dem Bildschirm anpassen, aber sie ist im Grunde genommen nichts anderes als eine „aufgebohrte“ Webseite.

Eine PWA benötigt im Grunde genommen nur vier wesentliche Hauptkomponenten:

  • Eine responsive Webseite, die auf allen anvisierten Endgeräten zuverlässig funktioniert. Dieses HTML-Dokument mit CSS und JavaScript stellt die Benutzeroberfläche und Navigation bereit.
  • Ein Web-Manifest: Diese JSON-Datei enthält wesentliche Informationen über die App wie ihren Namen, das Icon, Farbschema der Benutzeroberfläche, Start-URL und Anzeigemodus;
  • Einen Service-Worker: Dieses Skript läuft auf dem Endgerät im Hintergrund und fängt die Netzwerkanfragen ab, um die App mit Hilfe einer Cache-API offline verfügbar zu machen;
  • Eine HTTPS-Verbindung zum Webserver: Eine sichere Verbindung ist unter anderem Voraussetzung für den Einsatz eines Service-Workers.

Wie entwickelt man eigentlich eine PWA?

Die Installierbarkeit ist einer der Hauptunterschiede zwischen klassischen Webseiten und ihren progressiven Gegenstücken. Damit sich eine progressive Webapp installieren lässt, benötigt sie ein Manifest und einen Service-Worker. Die übrige Funktionalität ist optional. Den meisten Teams stehen also die folgenden Arbeitsschritte bevor:

Schritt 1: Erstellen Sie eine responsive Webseite, die auf allen Geräten eine Grundfunktionalität bereitstelle.

Schritt 2: Erstellen Sie ein Manifest und verzeichnen Sie es im Markup der responsiven Webseite.

Schritt 3: Registrieren Sie einen Service-Worker und binden Sie die zugehörige JavaScript-Datei korrekt in das Projekt ein.

Schritt 4: Legen Sie sich auf eine Cache-Strategie fest und implementieren Sie diese. Testen Sie die Offline-Funktionalität.

Schritt 5: (optional) Implementieren Sie Push-Benachrichtigungen.

Security-Check; Die Kommunikation der PWA von Lufthansa hat DigiCert als vertrauenswürdig eingestuft.(Bild:  Lufthansa)
Security-Check; Die Kommunikation der PWA von Lufthansa hat DigiCert als vertrauenswürdig eingestuft.
(Bild: Lufthansa)

Schritt 6: Veröffentlichen Sie die PWA auf einem Webserver und sichern Sie diese mit einem HTTPS-Zertifikat ab.

Schritt 7: (optional) Verpacken Sie Ihre PWA für Marktplätze und reichen Sie sie ein.

Ab in die Startlöcher mit responsivem Design

Damit PWAs auf allen Geräten mit einem beliebigen Formfaktor und unabhängig von der Bildschirmausrichtung eine optimale Benutzererfahrung gewährleisten, ansprechend aussehen und zuverlässig funktionieren, kommt bei der Entwicklung der Benutzeroberfläche responsives Webdesign zum Tragen. Media-Queries (@media) und flexible Layouts machen es möglich.

Der sichtbare Bereich einer Webseite ist der sogenannte Viewport, steuerbar mit dem Tag:

<meta name="viewport" content="width=device-width, initial-scale=1">

Übersteigt die Größe der Webseite jene des Viewports, muss der User scrollen oder zoomen, um den gesamten Inhalt (notfalls ausschnittsweise) zu Gesicht zu bekommen. Im responsiven Design sorgen die sogenannten Media-Queries (@media) dafür, dass der Browser aus mehreren optimierten CSS-Layouts – anhand von Kriterien wie Bildschirmgröße, -Ausrichtung, Auflösung und anderen – diejenige Variante auswählt, die auf dem betreffenden Viewport die beste Benutzererfahrung bietet.

Für Inhalte in einem Grid-Layout lässt sich zum Beispiel die Anzahl der Spalten basierend auf der Bildschirmgröße und/oder -Ausrichtung in CSS ändern, zum Beispiel:

/* Cascading Stylesheet */
/* Mobile Standard-Ansicht: Einträge stapeln sich vertikal */
.grid-item {
   width: 100%;
}
/* Tablet Ansicht: Einträge in 2 Spalten anzeigen */
@media (min-width: 600px) {
   .grid-item {
      width: 50%;
   }
}
/* Desktop Ansicht: Einträge in 4 Spalten anzeigen */
@media (min-width: 1200px) {
   .grid-item {
      width: 25%;
   }
}

Sobald das Frontend Hand und Fuß hat, geht es ans Eingemachte: das Webmanifest und den Service-Worker.

PWA-Eigenschaften „manifestieren“

Eine Manifest-Datei mit dem Namen manifest.json legt fest, wie die PWA aussieht und sich verhält. Das HTML-Hauptdokument der PWA muss das Manifest im Header referenzieren:

<link rel="manifest" href="/manifest.json">

Das Manifest definiert das App-Icon (in mehreren Größen), den Namen und die Start-URL, aber auch das Farbschema, die Ausrichtung des Displays oder verwandte PWAs, die die Funktionalität der Hauptanwendung erweitern können. Zum Beispiel:

{   "name": "DevInsider App",
   "short_name": "DevInsider",
   "description": "Eine echte PWA.",
   "start_url": "/",
   "display": "standalone",
   "background_color": "#ffffff",
   "theme_color": "#000000",
   "orientation": "portrait",
   "icons": [
      {
         "src": "icon-192x192.png",
         "sizes": "192x192",
         "type": "image/png"
      },
      {
         "src": "icon-512x512.png",
         "sizes": "512x512",
         type": "image/png"
      }
   ]
}

Doch das Herzstück einer PWA ist weder eine responsive Webseite noch das Webmanifest, sondern der sogenannte Service-Worker.

Service-Worker registrieren

Bei einem Service Worker handelt es sich um ein Skript, das im Hintergrund läuft und das Verhalten der PWA koordiniert. Das Erstellen eines Service Workers für eine PWA ist ein entscheidender Schritt, um Funktionen wie das Caching von Ressourcen für den Offline-Zugriff, die Hintergrundsynchronisierung und Push-Benachrichtigungen bereitstellen zu können.

Ein Service-Worker in den Chrome DevTools.(Bild:  BMW / Google)
Ein Service-Worker in den Chrome DevTools.
(Bild: BMW / Google)

Um einen Service-Worker ins Leben zu rufen, speichern Sie eine neue Skriptdatei im Stammverzeichnis der Webanwendung (/service-worker.js). Danach müssen Sie den Service-Worker in einer registrierten JavaScript-Datei oder mit einem <script>-Tag in der HTML-Datei verzeichnen. Zum Beispiel:

if ('serviceWorker' in navigator) {
   navigator.serviceWorker.register('/service-worker.js')
   .then((registration) => {
      console.log('Service Worker mit Geltungsbereich:', registration.scope, ' erfolgreich registriert');
   })
   .catch((error) => {
      console.error('Die Registrierung des Service-Workers ist fehlgeschlagen:', error);
   });
}

Nach der Registrierung kann der Service Worker – entsprechenden Code vorausgesetzt – Netzwerkanfragen abfangen, Ressourcen cachen und andere Hintergrundaufgaben ausführen. Der Service-Worker kann etwa auf eine Fetch-Anfrage mit zwischengespeicherten Assets antworten, falls sich solche im Cache befinden sollten, oder auf das Netzwerk zurückgreifen, um die Daten vom Server einzuholen. Zum Beispiel:

Jetzt Newsletter abonnieren

Täglich die wichtigsten Infos zu RZ- und Server-Technik

Mit Klick auf „Newsletter abonnieren“ erkläre ich mich mit der Verarbeitung und Nutzung meiner Daten gemäß Einwilligungserklärung (bitte aufklappen für Details) einverstanden und akzeptiere die Nutzungsbedingungen. Weitere Informationen finde ich in unserer Datenschutzerklärung. Die Einwilligungserklärung bezieht sich u. a. auf die Zusendung von redaktionellen Newslettern per E-Mail und auf den Datenabgleich zu Marketingzwecken mit ausgewählten Werbepartnern (z. B. LinkedIn, Google, Meta).

Aufklappen für Details zu Ihrer Einwilligung
self.addEventListener('fetch', (event) => {
   event.respondWith(
      caches.match(event.request)
      .then((response) => {
         if (response) {
            return response; // Die Daten kommen aus dem Cache…
         }
         return fetch(event.request); // … oder eben nicht. Also, jetzt geht es zurück ins Netzwerk!
      })
   );
});

In der Datei service-worker.js lässt sich jetzt das Caching und andere Aspekte des Verhaltens der PWA steuern.

Offline-Modus: die Background Sync API

Die wichtigsten Funktionen einer PWA sollten auch ohne eine Internetverbindung verfügbar sein. Der Offline-Modus mit Datensynchronisierung erlaubt es Nutzerinnen und Nutzern, auch dann mit der App zu interagieren, wenn das Endgerät keinen Zugriff auf das Netzwerk haben sollte.

Die PWA wird bei der ersten Gelegenheit zur automatischen Hintergrundsynchronisierung die Benutzeraktionen automatisch ausführen und die lokal gespeicherten Inhalte mit dem Server abgleichen. Im Falle einer Messaging-App kann der User im Offline-Modus Nachrichten verfassen und abschicken, ohne auf die Wiederherstellung der Netzwerkverbindung oder gar auf die eigentliche Übermittlung warten zu müssen.

Eine PWA, die Offline-Interaktionen und Datensynchronisierung im Hintergrund unterstützt, verwendet in der Regel eine Kombination aus Service-Workern, der Cache Storage API und der Background Sync API. Die Background Sync API empfiehlt sich insbesondere für Anwendungen, die beispielsweise das Senden von Formulardaten, die offline eingegeben wurden, unterstützen sollten.

Ist der Service-Worker nach der Registrierung mittels …

navigator.serviceWorker.register('/service-worker.js');

… einsatzbereit (navigator.serviceWorker.ready), kann die App eine einmalige Synchronisierung anfordern, und zwar mittels:

navigator.serviceWorker.ready.then(function(swRegistration) {
   return swRegistration.sync.register('einSync');
});

Ist die Internetverbindung des Benutzers gerade unterbrochen, wird der Service-Worker die Synchronisierung automatisch durchführen, sobald sie wiederhergestellt ist. Damit es gelingt, lauscht im Service-Worker in der Datei /service-worker.js ein Event-Listener und wartet auf das sync-Ereignis:

self.addEventListener('sync', function(event) {
   if (event.tag == 'einSync') {
      event.waitUntil(jetztAber());
   }
});

Sobald ein sync-Ereignis mit dem Tag einSync eintrifft, legt die Funktion jetztAber() los. Hier gehört die eigentliche Synchronisierungslogik hin, also etwa Anweisungen zum Versenden von Daten an einen Server oder zum Abrufen von Updates. Ohne eine ausgereifte Fehlerbehandlung nützt es dennoch alles nichts. Netzwerkanfragen und Datenbankoperationen sind fehlerträchtig und bedürfen besonderer Aufmerksamkeit.

Die Aktualisierung einer PWA erfolgt serverseitig und bedarf keiner Zustimmung der Anwender und Anwenderinnen. Das soll jetzt natürlich nicht heißen, dass man die User ignorieren sollte. Eine Benachrichtigung ist das mindeste und es empfiehlt sich, nicht benötigte Daten aus dem Cache zu entfernen, um Speicherplatz freizugeben. Das gehört zum guten Stil.

Caching mit IndexedDB

IndexedDB ist ein NoSQL-Speichersystem im Browser, das Daten im Cache speichern und abrufen kann, um eine PWA um Offline-Unterstützung zu erweitern. Da nicht alle Browser diese Funktionalität bereitstellen, muss man sie erst abfragen, zum Beispiel mittels:

if (!('indexedDB' in window)) {
   console.log('Pech aber auch, keine IndexedDB');
   return;
}

Bevor sich Daten darin speichern lassen, müssen wir erst einmal die betreffende Datenbank öffnen und dabei alle erdenklichen Schicksalsschläge (sprich: Fehler) selbst im Code handhaben:

let db;
const request = indexedDB.open("Dev-InsiderDB", 1);
request.onerror = function(event) {
   console.log("Fehler beim Öffnen der DB", event);
};
request.onsuccess = function(event) {
   db = event.target.result;
};

Wenn die Datenbank zum ersten Mal geöffnet wird oder wenn sich die Version ändert, sollte man einen Object-Store erstellen:

request.onupgradeneeded = function(event) {
   let db = event.target.result;
   let objectStore = db.createObjectStore("Dev-Insider-ObjectStore", { keyPath: "id" });
};

Jetzt ist es auch möglich, Daten zum erstellten Object-Store hinzuzufügen …

let transaction = db.transaction(["Dev-Insider-ObjectStore"], "readwrite");
let objectStore = transaction.objectStore("Dev-Insider-ObjectStore");
let data = { id: 1, name: "Markus", alter: 32, stadt: "Stuttgart" };
objectStore.add(data);

… und wieder daraus abrufen, verändern und dergleichen damit anstellen:

let transaction = db.transaction(["Dev-Insider-ObjectStore"]);
let objectStore = transaction.objectStore("Dev-Insider-ObjectStore");
let request = objectStore.get(1);
request.onerror = function(event) {
   console.log("Fehler", event);
};
request.onsuccess = function(event) {
   console.log(request.result);
};

Wenn die App die Datenbank nicht mehr benutzt, gilt zu guter Letzt:

db.close();

IndexedDB arbeitet asynchron. Es empfiehlt sich daher, immer Ereignishandler oder Promises zu verwenden, um die Ergebnisse auszuwerten. Um den Code zur Handhabung der IndexedDB lesbarer zu gestalten, empfiehlt sich die Verwendung von Promises oder Async/Await. Bibliotheken wie idb bieten eine Promise-basierte API für IndexedDB. Außerdem müssen wir auch für das Handhaben von Fehlern Sorge tragen, um eine reibungslose User Experience zu gewährleisten.

Push-Benachrichtigungen

Push-Benachrichtigungen erlauben es der PWA, Benutzer über wichtige Updates oder Nachrichten zu informieren. Auch dies sind alles Aufgaben eines Service-Workers. Push-Benachrichtigungen in einer PWA machen sich eine Kombination aus der Push API, der Notifications API und Service-Workern zu Nutze.

Im folgenden Beispiel hört der Service-Worker auf das push-Ereignis und zeigt eine Benachrichtigung an:

self.addEventListener('push', function(event) {   var options = {
      body: event.data.text(),
      icon: 'images/icon.png',
      badge: 'images/badge.png'
   };
   event.waitUntil(
      self.registration.showNotification('Hier gibt es nichts Neues!', options)
   );
});

Push-Benachrichtigungen erlauben es, die User auf ihren mobilen Endgeräten auch dann zu erreichen, wenn die PWA gar nicht geöffnet ist. Doch ganz so einfach ist es nicht. Denn die App muss vorab eine explizite Erlaubnis einholen, um Push-Benachrichtigungen zu erhalten, ein Push-Abonnement erstellen und die Erlaubnis auch noch verwalten. Das eigentliche Versenden von Push-Nachrichten bedarf einer serverseitigen Implementierung.

Bibliotheken wie web-push (Node.js), pywebpush (Python) und php-web-push (PHP) können hier Abhilfe schaffen. Um Push-Benachrichtigungen sicher zu versenden, kommen Sie nicht umhin, einen VAPID-Schlüssel (Voluntary Application Server Identification) zu generieren. Auch dafür gibt es Tools und Bibliotheken, darunter VAPID Key Generator oder die Firefox-Erweiterung VAPID Helper, und unzählige andere für Sprachen wie Java, Go und Ruby.

Hoste es, was es wolle

Schließlich muss man die Webseite auf einem sicheren Server hosten (oder alternativ einen lokalen Server mit einem selbstsignierten Zertifikat einrichten, um die App lokal zu testen oder im LAN hinter einer Firewall zu nutzen).

Die Verwendung von Service-Workern setzt in der Produktion eine HTTPS-Verbindung voraus. Für Entwicklungszwecke genügt aber localhost mit einem selbst signierten Zertifikat. Für einen vollqualifizierten Domainnamen empfiehlt sich eine respektable Zertifikatsstelle wie beispielsweise Let's Encrypt.

Um die PWA zu veröffentlichen, genügt es, nach der Bereitstellung den Benutzern einen Weblink oder einen QR-Code bereitzustellen.

Was am Ende bleibt

Eine PWA sollte eine großartige Benutzererfahrung nach dem Mobile-first-Paradigma bieten und eine höhere Konversionsrate als eine reine Webseite anstreben. Intuitive Navigation, klare Call-to-Action-Elemente und ein ansprechendes Design gehören ins Pflichtenheft. Sind einige grundlegende Voraussetzungen erfüllt, lassen sich mit PWAs leistungsstarke Features und immersive Inhalte benutzerfreundlich, plattformübergreifend und nicht zuletzt kosteneffizient bereitstellen.

(ID:49795111)