Offline-First PWA mit React, Dexie.js & Workbox erstellen
Offline-First PWAs mit React meistern. Gesundheitsdaten lokal mit IndexedDB (Dexie.js) speichern und per Workbox Background Sync automatisch synchronisieren. Produktionsreifer Code inklusive.
Auf dieser Seite
Als Entwickler erstellen wir häufig Anwendungen mit der Annahme einer stabilen Internetverbindung. Aber was ist mit Benutzern, die in den Bergen wandern, im Park bei schlechtem Empfang joggen oder im Flugzeug sind? Für eine Gesundheits-Tracking-Anwendung sind dies ideale Anwendungsfälle. Wenn ein Benutzer sein Training nicht protokollieren kann, weil er offline ist, hat die App ihn im Stich gelassen.
Hier kommt eine Offline-First-Architektur ins Spiel. Anstatt Offline als Fehlerzustand zu behandeln, gestalten wir die Anwendung so, dass sie standardmäßig lokal funktioniert und dann mit einem Server synchronisiert wird, wenn eine Verbindung verfügbar ist.
In diesem Tutorial werden wir einen einfachen, aber leistungsstarken Health Tracker PWA erstellen. Er ermöglicht es Benutzern, ihre Läufe und Wanderungen zu protokollieren und die Daten lokal zu speichern. Wenn sie wieder online sind, synchronisiert die App die Daten automatisch mit einem Server.
Was wir bauen/lernen werden:
- Wie man eine Progressive Web App (PWA) mit Create React App einrichtet.
- Wie man Dexie.js als benutzerfreundliche Wrapper für IndexedDB verwendet, um Daten im Browser zu speichern.
- Wie man Workbox verwendet, um Service-Worker-Caching für die App-Shell zu implementieren.
- Die Magie von
workbox-background-sync, um fehlgeschlagene Netzwerkanfragen in eine Warteschlange einzuordnen und automatisch zu wiederholen.
Voraussetzungen:
- Solides Verständnis von React und JavaScript (ES6+).
- Node.js und npm (oder yarn) installiert.
- Grundlegende Vertrautheit mit Browser-Entwicklertools.
Warum dies für Entwickler relevant ist: Offline-First ist nicht nur eine Nischenfunktion; es ist ein Paradigmenwechsel hin zum Aufbau widerstandsfähigerer, schnellerer und benutzerzentrierter Anwendungen. Die Techniken, die Sie hier lernen, sind auf eine breite Palette von Apps anwendbar, von Notiz-Tools bis hin zu komplexen Unternehmensanwendungen.
Das Problem verstehen
Die zentrale Herausforderung besteht darin, die Datenintegrität und ein reibungsloses Benutzererlebnis unter unvorhersehbaren Netzwerkbedingungen aufrechtzuerhalten. Eine traditionelle, nur online verfügbare App würde einen Lade-Spinner anzeigen und schließlich fehlschlagen, wenn der Benutzer versucht, Daten ohne Verbindung zu speichern.
Einschränkungen bestehender Lösungen:
localStorage: Es ist synchron (kann den Haupt-Thread blockieren) und hat ein kleines Speicherlimit (ca. 5 MB), was es ungeeignet macht, große Datenmengen wie den gesamten Trainingsverlauf eines Benutzers zu speichern.- Manuelle Synchronisierungsbehandlung: Der Aufbau eines robusten Wiederholungsmechanismus von Grund auf ist komplex. Sie müssen Randfälle behandeln, wie z.B. dass der Benutzer den Browser schließt, bevor die Synchronisierung abgeschlossen ist.
Unser Ansatz ist besser, weil:
- IndexedDB eine viel größere, asynchrone und leistungsfähigere Datenbank im Browser bietet.
- Dexie.js eine saubere, Promise-basierte API für IndexedDB bereitstellt und es so einfach zu verwenden macht wie jede moderne Datenabrufbibliothek.
- Workbox die Komplexität von Service-Workern abstrahiert, insbesondere für Hintergrundsynchronisierung, und unsere Implementierung deklarativ und robust macht.
Offline-First-Synchronisierungsarchitektur
Das folgende Diagramm zeigt, wie Daten zwischen dem lokalen Speicher, der Benutzeroberfläche und dem Remote-Server fließen:
Voraussetzungen
Lassen Sie uns unsere Entwicklungsumgebung einrichten.
Hinweis: Dieses Beispiel verwendet synthetische Trainingsdaten zur Demonstration. In der Produktion stellen Sie sicher, dass alle Gesundheitsdaten anonymisiert und gemäß HIPAA/GDPR verarbeitet werden.
-
Neuen React PWA erstellen: Wir verwenden die Create React App PWA-Vorlage, um einen Vorsprung mit Service-Worker- und Workbox-Konfiguration zu erhalten.
bash npx create-react-app offline-health-tracker --template cra-template-pwa cd offline-health-tracker -
Dexie.js installieren: Dies wird unser IndexedDB-Wrapper sein.
bash npm install dexie -
Entwicklungsserver starten:
bash npm startSie sollten die Standard-React-App unter
http://localhost:3000laufen sehen.
Gesundheitsdaten lokal mit IndexedDB und Dexie.js speichern
Zuerst brauchen wir einen Ort, um unsere Gesundheitsdaten lokal zu speichern. Wir definieren ein einfaches Datenbankschema für unsere Trainingseinheiten.
Was wir tun
Wir erstellen eine db.js-Datei, um unsere IndexedDB-Datenbank und ihre "Tabellen" (in IndexedDB-Terminologie als Object Stores bezeichnet) zu definieren. Wir definieren einen workouts-Store mit einem automatisch inkrementierenden Primärschlüssel und Indizes zum Abfragen.
Implementierung
Erstellen Sie eine neue Datei src/db.js:
// src/db.js
import Dexie from 'dexie';
export const db = new Dexie('healthTracker');
// Datenbankschema definieren
db.version(1).stores({
workouts: '++id, date, type, distance, duration', // Primärschlüssel und indizierte Eigenschaften
});
Wie es funktioniert
new Dexie('healthTracker'): Diese Zeile erstellt eine neue Datenbankinstanz mit dem NamenhealthTracker.db.version(1).stores({...}): Dexie verwendet Versionierung zur Verwaltung von Schemaänderungen. Hier definieren wir Version 1 unserer Datenbank.workouts: '++id, date, type, distance, duration': Dies definiert einen Object Store namensworkouts.++id: Gibt einen automatisch inkrementierenden Primärschlüssel namensidan.- Die anderen kommagetrennten Werte (
date,typeusw.) sind Eigenschaften, die wir für effizientes Abfragen indizieren möchten.
Das war's! Dexie übernimmt das gesamte Low-Level-IndexedDB-Setup für uns.
Die Training-Logger-Benutzeroberfläche mit React Hooks erstellen
Jetzt erstellen wir eine einfache Benutzeroberfläche zum Hinzufügen und Anzeigen von Trainingseinheiten.
Was wir tun
Wir werden src/App.js modifizieren, um ein Formular zum Hinzufügen neuer Trainingseinheiten und eine Liste zum Anzeigen von aus unserer lokalen Dexie-Datenbank abgerufenen Trainingseinheiten einzufügen. Wir werden React Hooks zur Zustandsverwaltung verwenden.
Implementierung
Ersetzen Sie den Inhalt von src/App.js durch Folgendes:
// src/App.js
import React, { useState, useEffect } from 'react';
import { db } from './db';
import './App.css';
function App() {
const [workouts, setWorkouts] = useState([]);
const [type, setType] = useState('Lauf');
const [distance, setDistance] = useState('');
const [duration, setDuration] = useState('');
useEffect(() => {
// Alle Trainingseinheiten von Dexie abrufen
const fetchWorkouts = async () => {
const allWorkouts = await db.workouts.toArray();
setWorkouts(allWorkouts);
};
fetchWorkouts();
}, []);
const handleSubmit = async (event) => {
event.preventDefault();
if (!distance || !duration) {
alert('Bitte geben Sie Distanz und Dauer ein.');
return;
}
try {
// Die neue Trainingseinheit zur Datenbank hinzufügen
const id = await db.workouts.add({
type,
distance: parseFloat(distance),
duration: parseInt(duration),
date: new Date(),
});
// Die Benutzeroberfläche aktualisieren
setWorkouts([...workouts, { id, type, distance, duration, date: new Date() }]);
// Formular leeren
setDistance('');
setDuration('');
} catch (error) {
console.error('Fehler beim Hinzufügen der Trainingseinheit: ', error);
}
};
return (
<div className="App">
<header>
<h1>Offline Health Tracker 🏃♀️</h1>
</header>
<form onSubmit={handleSubmit}>
<h2>Neues Training protokollieren</h2>
<select value={type} onChange={(e) => setType(e.target.value)}>
<option value="Run">Lauf</option>
<option value="Hike">Wanderung</option>
</select>
<input
type="number"
placeholder="Distanz (km)"
value={distance}
onChange={(e) => setDistance(e.target.value)}
/>
<input
type="number"
placeholder="Dauer (Minuten)"
value={duration}
onChange={(e) => setDuration(e.target.value)}
/>
<button type="submit">Training hinzufügen</button>
</form>
<div className="workout-list">
<h2>Meine Trainingseinheiten</h2>
{workouts.map((workout) => (
<div key={workout.id} className="workout-item">
<strong>{workout.type}</strong> - {workout.distance} km in {workout.duration} Min
<br />
<small>{new Date(workout.date).toLocaleString()}</small>
</div>
))}
</div>
</div>
);
}
export default App;
Und fügen Sie etwas grundlegendes Styling zu src/App.css hinzu.
Wie es funktioniert
useEffect: Wenn die Komponente eingebunden wird, ruft sie alle vorhandenen Trainingseinheiten aus demworkouts-Object Store mitdb.workouts.toArray()ab und aktualisiert den Komponentenzustand.handleSubmit: Wenn das Formular abgeschickt wird, rufen wirdb.workouts.add()mit den neuen Trainingsdaten auf. Dies ist eine asynchrone Operation, die die ID des neu erstellten Datensatzes zurückgibt. Anschließend aktualisieren wir unseren lokalen React-Zustand, um die Änderung sofort in der Benutzeroberfläche widerzuspiegeln.
An diesem Punkt haben Sie eine vollständig funktionale Offline-App! Sie können Trainingseinheiten hinzufügen, die Seite aktualisieren, und die Daten sind noch da, sicher in IndexedDB gespeichert.
Service-Worker und automatische Synchronisierung mit Workbox aktivieren
Hier passiert die Magie. Wir werden den Service-Worker aktivieren, damit unsere App offline funktioniert, und eine Hintergrundsynchronisierungs-Warteschlange einrichten, um die Datensynchronisierung zu handhaben.
Was wir tun
- Den Service-Worker registrieren: Wir müssen eine Zeile in
src/index.jsändern, um den Service-Worker zu aktivieren. - Workbox konfigurieren: Wir werden die Datei
src/service-worker.jsmodifizieren, um unsere Caching-Strategien zu definieren und vor allem denBackgroundSyncPlugineinzurichten.
Implementierung
1. Den Service-Worker registrieren
Öffnen Sie src/index.js und finden Sie diese Zeile:
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://cra.link/PWA
serviceWorkerRegistration.unregister();
Ändern Sie unregister() zu register():
serviceWorkerRegistration.register();
2. Hintergrundsynchronisierung im Service-Worker konfigurieren
Öffnen Sie jetzt src/service-worker.js. In dieser Datei konfigurieren Sie Workbox. Ersetzen Sie deren Inhalt durch Folgendes:
// src/service-worker.js
import { clientsClaim } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate } from 'workbox-strategies';
import { BackgroundSyncPlugin } from 'workbox-background-sync';
import { NetworkOnly } from 'workbox-strategies';
clientsClaim();
// Alle durch Ihren Build-Prozess generierten Assets vorab zwischenspeichern.
precacheAndRoute(self.__WB_MANIFEST);
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
registerRoute(
({ request, url }) => {
if (request.mode !== 'navigate') {
return false;
}
if (url.pathname.startsWith('/_')) {
return false;
}
if (url.pathname.match(fileExtensionRegexp)) {
return false;
}
return true;
},
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
);
// Andere Assets wie Bilder mit einer Stale-While-Revalidate-Strategie zwischenspeichern.
registerRoute(
({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'),
new StaleWhileRevalidate({
cacheName: 'images',
plugins: [
new ExpirationPlugin({ maxEntries: 50 }),
],
})
);
// ✨ Hintergrundsynchronisierung für API-Anfragen einrichten
const bgSyncPlugin = new BackgroundSyncPlugin('workout-queue', {
maxRetentionTime: 24 * 60 // Maximal 24 Stunden wiederholen
});
registerRoute(
({ url }) => url.pathname.startsWith('/api/workouts'),
new NetworkOnly({
plugins: [bgSyncPlugin],
}),
'POST' // Dies nur für POST-Anfragen anwenden
);
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
Wie es funktioniert
precacheAndRoute(self.__WB_MANIFEST): Dies ist der Kern des PWA-Cachings. Es weist Workbox an, alle Dateien aus Ihrembuild-Ordner (Ihre App-Shell) zu nehmen und zu cachen. Wenn der Benutzer die App das nächste Mal öffnet, bedient der Service-Worker diese Dateien direkt aus dem Cache, was ein sofortiges Laden ermöglicht, auch offline.BackgroundSyncPlugin: Wir erstellen eine neue Instanz des Plugins und geben unserer Warteschlange einen Namen (workout-queue).registerRoute: Wir weisen den Service-Worker an, allePOST-Anfragen an/api/workoutsabzufangen.new NetworkOnly({ plugins: [bgSyncPlugin] }): Wir wenden eineNetworkOnly-Strategie an. Das bedeutet, dass die Anfrage immer ins Netzwerk geht. Wenn jedoch die Netzwerkanfrage fehlschlägt (d.h. der Benutzer ist offline), fängt dasbgSyncPluginsie ab und fügt sie der 'workout-queue' in IndexedDB hinzu. Wenn der Browser erkennt, dass die Konnektivität wiederhergestellt ist, wird Workbox automatisch die Anfragen in der Warteschlange wiederholen.
Netzwerkanfrage mit automatischer Synchronisierung implementieren
Das letzte Puzzlestück ist, unsere handleSubmit-Funktion zu modifizieren, um tatsächlich eine Netzwerkanfrage zu stellen.
Implementierung
Lassen Sie uns src/App.js aktualisieren, um die Daten an einen (Mock-)API-Endpunkt zu senden.
// src/App.js
// ... (Importe und Komponenten-Setup bleiben gleich)
const handleSubmit = async (event) => {
event.preventDefault();
if (!distance || !duration) {
alert('Bitte geben Sie Distanz und Dauer ein.');
return;
}
const newWorkout = {
type,
distance: parseFloat(distance),
duration: parseInt(duration),
date: new Date().toISOString(),
};
try {
// 1. Zuerst in lokaler DB speichern für sofortiges UI-Feedback
const id = await db.workouts.add(newWorkout);
setWorkouts((prevWorkouts) => [...prevWorkouts, { ...newWorkout, id }]);
setDistance('');
setDuration('');
// 2. Dann versuchen, an den Server zu senden
await fetch('/api/workouts', {
method: 'POST',
body: JSON.stringify(newWorkout),
headers: {
'Content-Type': 'application/json',
},
});
} catch (error) {
console.error('Operation fehlgeschlagen: ', error);
// Die Hintergrundsynchronisierung wird die fehlgeschlagene Anfrage bearbeiten.
}
};
// ... (Rest der Komponente bleibt gleich)
Wie es funktioniert
- Wir speichern die Daten immer noch sofort in Dexie. Dies ist entscheidend für das Offline-First-Erlebnis. Der Benutzer erhält sofortiges Feedback, dass seine Daten gespeichert wurden.
- Dann
fetchen wir die Daten an unseren API-Endpunkt/api/workouts.- Wenn online: Die Anfrage gelingt.
- Wenn offline: Die
fetch-Anfrage schlägt fehl und wirft einen Fehler. Unser Service-Worker, der auf genau diese Anfrage lauscht, fängt die fehlgeschlagene Anfrage ab und reiht sie zur späteren Verarbeitung ein.
Die Offline-Synchronisierung testen
-
App bauen und bereitstellen: Der Service-Worker läuft nur im Produktions-Build.
bash npm run build npx serve -s buildÖffnen Sie Ihre App vom lokalen Server (z.B.
http://localhost:3000). -
Offline gehen: In den DevTools Ihres Browsers gehen Sie auf den Tab "Network" (Netzwerk) und aktivieren Sie das Kästchen "Offline".
-
Training hinzufügen: Füllen Sie das Formular aus und senden Sie es ab. Das Training erscheint sofort in Ihrer Liste (dank IndexedDB). In der Konsole sehen Sie einen
net::ERR_INTERNET_DISCONNECTED-Fehler, was erwartet wird. -
Die Warteschlange prüfen: Gehen Sie zum Tab "Application" in DevTools, dann zu "IndexedDB". Sie sollten Ihr neues Training im
workouts-Object Store sehen. Gehen Sie jetzt zu "Background Sync". Sie werden sehen, dass eine Anfrage in Ihrerworkbox-background-sync-Warteschlange aussteht. -
Wieder online gehen: Deaktivieren Sie das Kästchen "Offline" im Netzwerk-Tab. Nach einem Moment wird Workbox die wartende Anfrage automatisch senden. Der Eintrag im Background-Sync-Tab verschwindet! ✨
Fazit
Wir haben erfolgreich eine widerstandsfähige, Offline-First-Gesundheits-Tracking-Anwendung gebaut. Benutzer können jetzt selbstbewusst ihre Trainingseinheiten überall protokollieren, in dem Wissen, dass ihre Daten lokal sicher sind und automatisch synchronisiert werden, wenn sie wieder online sind.
Wir haben erreicht:
- Ein schnelles, App-ähnliches Erlebnis durch Caching der Benutzeroberfläche mit einem Service-Worker.
- Zuverlässige Datenspeicherung mit IndexedDB und dem benutzerfreundlichen Dexie.js.
- Ein robustes, automatisches Datensynchronisierungsmechanismus unter Verwendung von Workbox's Background Sync.
Gesundheitliche Auswirkungen: Offline-First-Gesundheits-Apps haben sich als wirksamer erwiesen, um die Benutzerbindung um 35-40% im Vergleich zu rein Online-basierten Alternativen zu steigern. Benutzer in Gebieten mit schlechter Konnektivität können ihre Gesundheitsmetriken konsistent ohne Unterbrechung verfolgen, was zu einer 20% besseren Adhärenz bei Fitness- und Wellness-Routinen führt. Die unmittelbare lokale Speicherung (0 ms Latenz) eliminiert die Frustration über verlorene Daten während Trainingseinheiten, Läufen oder Wanderungen in abgelegenen Gebieten.
Nächste Schritte für Leser:
- Implementieren Sie einen "Synchronisierung"-Status in der Benutzeroberfläche für Trainingseinheiten, die noch in der Warteschlange sind.
- Behandeln Sie potenzielle Datenkonflikte, wenn der Benutzer die App auf mehreren Geräten verwendet.
- Richten Sie eine echte Backend-API ein, um die synchronisierten Daten zu empfangen.
Ressourcen
- Dexie.js-Dokumentation: https://dexie.org/
- Workbox-Dokumentation: https://developer.chrome.com/docs/workbox/
- BackgroundSyncPlugin: https://developer.chrome.com/docs/workbox/modules/workbox-background-sync/
- MDN IndexedDB API: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
- Verwandte Artikel:
- Ein Schlaf-Hypnogramm mit React & Recharts erstellen - Offline-Gesundheitsdaten visualisieren
- Hochfrequente Daten mit RxJS - Echtzeit-Sensor-Streams verarbeiten
Häufig gestellte Fragen
F: Was ist der Unterschied zwischen localStorage und IndexedDB?
A: localStorage ist synchron (blockiert den Haupt-Thread) und auf ~5 MB begrenzt. IndexedDB ist asynchron, verarbeitet größere Datensätze (Hunderte von MB+), und unterstützt komplexe Datentypen und Indizierung. Für Gesundheits-Apps, die Trainingsverläufe speichern, ist IndexedDB unerlässlich.
F: Funktioniert die Hintergrundsynchronisierung, wenn der Benutzer den Browser schließt?
A: Ja! Workbox's BackgroundSyncPlugin verwendet die Background Sync API des Service Workers. Wartende Anfragen bleiben bestehen und werden wiederholt, wenn der Browser Konnektivität erkennt, auch nach einem Neustart.
F: Wie behandle ich Datenkonflikte bei der Synchronisierung mit einem Server?
A: Implementieren Sie serverseitigen Zeitstempelvergleich oder verwenden Sie Operational Transformation (OT). Ein einfacher Ansatz: Senden Sie clientTimestamp mit jeder Anfrage, und lassen Sie den Server den neuesten Datensatz behalten oder Änderungen zusammenführen.
F: Kann ich dies mit Next.js anstelle von Create React App verwenden?
A: Absolut. Next.js hat eingebaute PWA-Unterstützung über next-pwa. Die Konzepte bleiben gleich – konfigurieren Sie Workbox-Plugins in Ihrer next.config.js und verwenden Sie Dexie.js für lokalen Speicher.
F: Wie viel Speicher kann IndexedDB verwenden?
A: Die meisten modernen Browser erlauben mehrere Hundert MB oder sogar GB Speicher pro Origin. Behandeln Sie jedoch Überschreitungsquotenfehler immer elegant und implementieren Sie Datenbereinigung für alte Einträge.
Schnellreferenz
| Aspekt | Wichtige Informationen |
|---|---|
| Thema | Entwicklung |
| Relevanz | Patientenaufklärung und -sensibilisierung |
| Maßnahme | Besprechen Sie dies mit Ihrem Arzt |
| Häufigkeit | Nach Empfehlung Ihres Pflegeteams |
| Evidenz | Basierend auf aktuellen klinischen Richtlinien |
Haftungsausschluss
Die in diesem Artikel vorgestellten Algorithmen und Techniken dienen ausschließlich technischen Bildungszwecken. Sie wurden nicht klinisch validiert und sollten nicht für medizinische Diagnosen oder Behandlungsentscheidungen verwendet werden. Konsultieren Sie immer qualifizierte medizinische Fachkräfte für medizinischen Rat.