Progressive Web Apps

PWAs bieten einen kosteneffizienten Weg, cross-platform Apps zu entwickeln. Nach diesem Post weißt du, was eine PWA ist und wie du ein einfache PWA selbst baust. Wir stellen außerdem weiteres Material zur Verfügung, womit du tiefer in das Thema einsteigen kannst.

Motivation

Was sind Progressive Web Apps?

Um zu verstehen, was Progressive Web Apps sind, müssen wir zunächst einen Blick auf die Unterschiede zwischen den Eigenschaften von Webseiten einerseits und nativen Apps andererseits werfen.

Webseiten sind immer aktuell. Sie können von jedem auf jedem Gerät erreicht werden. Sie benötigen nur eine einzige Codebase. Außerdem sind sie oft sehr zugänglich und reaktionsschnell sowie über Suchmaschinen leicht zu finden.

Native Apps sind tief in dein Gerät integriert. Sie befinden sich auf dem Startbildschirm oder in der Taskleiste und funktionieren unabhängig von der Netzwerkverbindung. Sie können auf deine Hardware zugreifen, z. B. deine Dateien lesen oder die Bluetooth-Verbindung nutzen. Plattformspezifische Apps können z. B. deine Kontakte verwalten, die Wiedergabe von Musiktiteln steuern, Push-Benachrichtigungen senden oder Fotos aufnehmen.

Progressive Web Apps bieten alle Möglichkeiten beider Welten. Sie werden auf der Grundlage von Webtechnologien entwickelt, können aber plattformspezifische Funktionen nutzen und ein natives Erlebnis bieten.

Wir haben bereits in einem früheren Blogbeitrag über die überraschende Vielseitigkeit von Web-Apps gesprochen. Falls du ihn nicht kennst, kannst du ihn jederzeit nachlesen. Er bietet einen kurzen historischen Abriss, der erklärt, warum PWAs entwickelt wurden und welche Anwendungsfälle es gibt.

Der Begriff "Progressive Web Apps" beschreibt eine App, die native Web APIs und die Features von modernen Browsern nutzt. Vor Allem sind das Service Workers und Web App Manifests.

Vor- und Nachteile

Die Vorteile von PWAs liegen auf der Hand: Sie bieten ein plattformübergreifendes, plattformspezifisches Erlebnis mit Offline- und Hintergrundbetrieb. Sie sind einfacher zu pflegen als mehrere native Apps und daher kostengünstiger. Sie können ebenso in diversen Stores wie dem Google Play Store, dem App Store oder dem Microsoft Store veröffentlicht werden. Darüber hinaus bieten sie automatische und nahtlose Aktualisierungen, so dass der Nutzer immer Zugriff auf die neueste Version hat.

Die Nachteile von PWAs sind: zuallererst die Abhängigkeit von der Browserunterstützung, da sie auf moderne Browserfunktionen angewiesen sind. Zweitens gibt es neben den meisten nativen APIs immer noch Funktionen, die ausschließlich nativen Anwendungen vorbehalten sind, z. B. Push-Benachrichtigungen auf iOS. Sie sind ressourcenintensiv und eignen sich nicht für Geräte mit begrenzten Ressourcen. Und schließlich sind sie auch nicht geeignet, um sehr plattformspezifische Funktionen wie die Windows API zu nutzen.

Ein weiteres Problem ist die allgemeine Awareness für die Existenz von PWAs. Wie oft hast du den "App installieren"-Badge oben rechts in deiner Suchleiste bemerkt?

Kompatibilität

Progressive Web Apps werden auf allen Plattformen, in unterschiedlichen Maßen, unterstützt. Am wichtigsten sind dabei Chromium-basierte Browser auf Windows, macOS, Linux und Android sowie bis zu einem gewissen Grad auch Safari auf iOS.

Table showing compatibility of PWAs on different browsers and devicesAb iOS 17.4 unterstützt Apple PWAs nicht mehr als Apps für den Startbildschirm. Es gibt zwar immer noch die Option "Zum Startbildschirm hinzufügen", aber statt einer eigenständigen App wird es nur noch ein Lesezeichen für deinen Standardbrowser sein. Außerdem sind Progressive Web Apps derzeit nicht für Apple Vision Pro verfügbar.

Wie bei allem im Web ist es wichtig, sich über die Grenzen und Möglichkeiten von PWAs bewusst zu sein und sich entsprechend anzupassen.

Limitations include the lack of push notifications, integration APIs (such as Web Bluetooth or WebNFC), and installation promotion techniques that help users know they can install the current website to get an app experience” (Source)

Anforderungen

Um eine PWA zu erstellen, brauchst du eine einfache Webseite und eine manifest.json Datei. Zusätzlich muss deine App als installierbar gelten. Schauen wir uns zunächst an, wie der Browser prüft, ob deine App installierbar ist.

Installierbarkeit

Die Voraussetzungen dafür, um deine Webseite installieren zu können, lassen sich kurz zusammenfassen:

  • Die Webanwendung ist nicht bereits installiert
  • Der Benutzer muss mit deiner Webseite interagiert haben - Die Seite mindestens einmal angeklickt - Mindestens 30 Sekunden auf der Seite verbracht
  • Die Webseite wird über HTTPS bereitgestellt
  • Eine manifest.json Datei ist verlinkt

Das "Web App Manifest"

Die Datei manifest.json enthält Metadaten über deine Anwendung. Zum Beispiel den Namen der App, die Start-URL, Icons für die App und einen Anzeigemodus. Der Anzeigemodus bestimmt, ob deine App wie eine Webseite mit einer Suchleiste oben oder wie eine eigenständige App aussieht.

Eine einfache Webseite

Du benötigst mindestens eine einfache index.html Datei mit etwas Inhalt und einen <link> zum Manifest im head deiner HTML Datei.

Außerdem benötigst du eine Möglichkeit, deine Website sicher über HTTPS bereitzustellen. Für die Entwicklung kann dies mit einem Tunneling-Tool wie ngrok erreicht werden.

PWA selbst bauen

Grundlegende App

Zu Beginn erstellen wir eine index.html Datei und fügen einen Verweis auf unsere manifest.json Datei hinzu. Wir ergänzen crossorigin="use-credentials" damit es später mit ngrok funktioniert.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <link rel="manifest" href="manifest.json" crossorigin="use-credentials" />
        <title>Document</title>
    </head>
    <body>
        <h1>Hello, World!</h1>
    </body>
</html>

Jetzt erstellen wir auch die manifest.json Datei.

{
    "name": "my pwa",
    "start_url": "/",
    "display": "standalone",
    "icons": [{
        "src": "icons/512.png",
        "type": "image/png",
        "sizes": "512x512"
    }]
}

Stelle sicher, dass du ein 512x512 icon bei icons/512.png bereitstellst. Der pwa-asset-generator ist für diese Aufgabe empfehlenswert.

Ich werde die Live Server Erweiterung von Visual Studio Code für die Bereitstellung der Webseite und ngrok für das Tunneling verwenden. Deine Webseite sollte jetzt installierbar sein. Das Debugging kann in den Developer Tools über den Tab "Application" oder über die Lighthouse Erweiterung von Chrome erfolgen.

Service Workers

Wenn du jetzt versuchst, deine App ohne Netzwerkverbindung zu öffnen, wirst du feststellen, dass deine HTML-Datei nicht angezeigt wird. An dieser Stelle kommen Service Worker ins Spiel. Es gibt zwei grundlegende Strategien, die wir verfolgen könnten:

“Offline first”: Versuche, die Datei aus dem Cache zu verwenden. Wenn keine Datei gefunden wird, hole die Datei vom Server.

“Network first”: Versuche, die Datei vom Server zu holen. Wenn der Server nicht verfügbar ist, verwende die Datei aus dem Cache.

Lass uns einen Service Worker erstellen, der die Strategie "Network first" verwendet. Wir erstellen eine neue Datei sw.js. Wir beginnen mit der Entscheidung, welche Dateien in den Cache aufgenommen werden sollen, und legen sie nach der initialen Installation im Cache ab.

const cacheName = "my-pwa-cache";
const files = ["/index.html", "/manifest.json", "/icons/512.png"];

self.addEventListener("install", (e) => {
    console.log("Service Worker: Installed");
    e.waitUntil(cacheFiles());
});

async function cacheFiles() {
    console.log("Service Worker: Caching Files");
    const cache = await caches.open(cacheName);
    await cache.addAll(files);
}

Nur weil unsere Dateien im Cache gespeichert sind, bedeutet das noch nicht, dass sie beim Öffnen unserer Anwendung automatisch verwendet werden. Wir müssen dem Service Worker sagen, wie ein Fetch gehandhabt werden soll:

self.addEventListener("fetch", async (e) => {
    e.respondWith(networkFirst(e.request));
});

async function networkFirst(request) {
    const cache = await caches.open(cacheName);

    // try to fetch from network
    console.log("Service Worker: Fetching from network");
    let response = await tryFetch(request, 200);
    if (response) {
        cache.put(request, response.clone());
        return response;
    }

    // try to fetch from cache
    console.log("Service Worker: Fetching from cache");
    response = await cache.match(request);
    if (response) {
        return response;
    }

    throw new Error("Both network and cache failed");
}

async function tryFetch(request, timeout) {
    const abortController = new AbortController();
    const timeoutId = setTimeout(() => abortController.abort(), timeout);

    try {
        return await fetch(request, { signal: abortController.signal });
    } catch {
        return null;
    } finally {
        clearTimeout(timeoutId);
    }
}

Zum Schluss müssen wir den Service Worker registrieren. Das machen wir, indem wir ein Skript in unsere index.html einfügen.

<script>
    if ("serviceWorker" in navigator) {
        navigator.serviceWorker.register("./sw.js");
    }
</script>

Das war's. Unsere Anwendung sollte jetzt offline funktionieren. Du kannst deinen Service Worker über den Tab "Application" in den Developer Tools einsehen und seine Logs sehen, indem du die Quelle oben links in der Konsole änderst.

Der Service Worker kann keine gecachte Version von sich selbst bereitstellen, da dies zu unerwarteten Problemen führen würde. Der Fetch-Request für den Service Worker kann nicht von einem Fetch-Handler eines anderen Service Workers abgefangen werden.

Kommunikation mit der App

Einige native Funktionen wie das Senden von Benachrichtigungen können nur von einem Service Worker aufgerufen werden. Aus diesem Grund ist eine grundlegende Kommunikation zwischen deinem Service Worker und der Anwendung (Client) erforderlich.

Der Service Worker kann mit der Funktion Client.postMessage() Nachrichten an den Client senden. Ein Beispiel dafür siehst du here.

Für die Kommunikation in beide Richtungen kann die Broadcast Channel API verwendet werden. Eine einfache Kommunikation kann wie folgt aussehen:

// connect to a channel
const bc = new BroadcastChannel("test");

// in the service worker
bc.postMessage("test message");

// in the app
bc.onmessage = (e) => console.log(e)

UI Anpassungen (optional)

Neben den Icons gibt es im manifest.json noch weitere Anpassungsmöglichkeiten. So kannst du beispielsweise eine Hintergrundfarbe festlegen, welche verwendet wird, bevor das ursprüngliche CSS lädt, und eine Themenfarbe definieren, die dann auf verschiedenen Betriebssystemen unterschiedlich interpretiert werden kann. Das Manifest bietet auch die Möglichkeit, "Verknüpfungen" für deine App zu definieren. Das ist das Kontextmenü, welches angezeigt wird, wenn du auf dem Desktop mit der rechten Maustaste auf deine App klickst oder auf dem Handy lange auf die App drückst.

Natürlich gibt es noch viele andere wertvolle Optionen, welche du hier finden kannst.

Nachdem wir nun eine grundlegende PWA erstellt haben, besteht der nächste Schritt darin, die neu erlangten Funktionen des Geräts auszuprobieren. Die Wahl der richtigen nativen API hängt weitgehend davon ab, was du tun möchtest. Eine PWA ohne native APIs würde nicht viel Sinn ergeben, da es sich sonst nur um eine installierbare Webseite handeln würde. Im nächsten Abschnitt werde ich kurz auf wichtige APIs eingehen, die du verwenden kannst.

Native APIs

Es gibt eine Vielzahl nützlicher APIs, die genutzt werden können. Eine vollständige Liste findest du hier. Ein paar wichtige APIs werden jetzt aufgelistet.

Ökosystem

Lighthouse

Lighthouse kann verwendet werden, um Probleme und mögliche Verbesserungen der Webanwendung zu finden.

A lighthouse report showing the performance of a PWA

Workbox

Die Verwendung einer Bibliothek wie Workbox kann die Entwicklung von PWAs erheblich beschleunigen. Das obige Beispiel für Network First Caching kann so mit viel weniger Code nachgebaut werden. Gleichzeitig ist es auch in hohem Maße anpassbar. Hier ist ein Beispiel für einen Service Worker mit Workbox:

importScripts("https://cdnjs.cloudflare.com/ajax/libs/workbox-sw/7.0.0/workbox-sw.min.js");

workbox.routing.registerRoute(
    () => true,
    new workbox.strategies.NetworkFirst()
);

Workbox speichert Anfragen, die dem in registerRoute() definierten Pattern entsprechen. In diesem Fall also alles.

Frontend Frameworks

Durch Hinzufügen von workbox zu deinem Frontend-Framework kannst du PWAs ohne großen Aufwand nutzen.

Für React bietet create-react-app alle Werkzeuge für die Erstellung einer PWA von out-of-the-box. Siehe hier für weitere Informationen.

Angular bietet ein eingebautes Service-Worker-Modul. Siehe hier für weitere Informationen.

Es gibt auch ein Plugin für Vite, um PWAs schnell und fast ohne Konfiguration einzustellen.

Fazit

Progressive Web Apps (PWAs) stellen einen bedeutenden Wandel im Web-Ökosystem dar und vereinen die Stärken von Web- und nativen Anwendungen. Sie bringen zwar eine Reihe von Herausforderungen mit sich, wie z. B. Browserabhängigkeit und Einschränkungen bei bestimmten nativen Funktionen, aber ihre Vorteile sind kaum zu übersehen. Von nahtlosen Updates und plattformübergreifender Kompatibilität bis hin zur Nutzung moderner Web-APIs für erweiterte Funktionen - PWAs definieren neu, was Webanwendungen leisten können. Während wir diese Technologien weiter erkunden und verwenden, wird klar, dass die Zukunft der Webentwicklung nicht nur in der Erstellung von Websites liegt, sondern in der Schaffung immersiver, app-ähnlicher Erlebnisse, die für ein breiteres Publikum zugänglich sind. Das sich entwickelnde Ökosystem, das Tools wie Lighthouse und Workbox sowie die Unterstützung durch verschiedene Frontend-Frameworks umfasst, macht die Entwicklung von PWAs zugänglicher und leistungsfähiger und verspricht eine spannende Zukunft für die Entwicklung von Webanwendungen.

Wenn du an einem anderen Thema interessiert bist, lies dir den Artikel Die Wahl der richtigen Dateistruktur in deinem Projekt durch. Unsere anderen Blogbeiträge findest du hier.