Offline-First Mood Journal PWA mit Next.js & IndexedDB erstellen
Erfahren Sie, wie Sie eine resiliente Offline-First Progressive Web App (PWA) mit Next.js erstellen. Das Tutorial behandelt Service Worker für Caching und IndexedDB für clientseitige Speicherung.
Auf dieser Seite
Psychische Gesundheit und Achtsamkeits-Apps sind beliebter als je zuvor. Doch was passiert, wenn Sie Ihre Stimmung während eines Flugs, in abgelegenen Gebieten oder einfach bei einer schlechten Internetverbindung erfassen möchten? Die meisten Web-Apps scheitern und zeigen die gefürchtete Seite „Keine Internetverbindung". Dies unterbricht das Benutzererlebnis und macht die App genau dann unbrauchbar, wenn sie am meisten gebraucht wird.
In diesem Tutorial gehen wir dieses Problem direkt an. Wir werden eine Stimmungs-Tagebuch-PWA mit Next.js erstellen, die „offline-first" ist. Das bedeutet, dass die Anwendung so konzipiert ist, dass sie ohne Netzwerkverbindung einwandfrei funktioniert. Alle Daten werden lokal in der eigenen Datenbank des Browsers, IndexedDB, gespeichert, und die App-Oberfläche wird dank eines Service Workers sofort aus einem Cache geladen.
Was wir bauen/lernen werden:
- Eine als PWA konfigurierte Next.js-Anwendung.
- Wie man Daten in IndexedDB speichert, liest und aktualisiert.
- Einrichten eines Service Workers mit
next-pwazum Cachen von Assets. - Eine saubere, funktionale Benutzeroberfläche für das Führen eines Stimmungstagebuchs.
Voraussetzungen:
- Solides Verständnis von React und JavaScript (ES6+).
- Node.js (v18 oder höher) und npm/yarn installiert.
- Grundlegende Vertrautheit mit Next.js ist hilfreich, aber nicht erforderlich.
Warum dies für Entwickler relevant ist: Der Aufbau von Offline-first-Anwendungen ist keine Nischenfertigkeit mehr. Es ist eine entscheidende Komponente für die Erstellung resilienter, zuverlässiger und leistungsstarker Web-Erlebnisse, die mit nativen Apps konkurrieren können.
Das Problem verstehen
Die zentrale Herausforderung besteht darin, den Zustand und die Funktionalität aufrechtzuerhalten, wenn das Netzwerk nicht verfügbar ist. Traditionelle Web-Apps sind auf eine ständige Verbindung zu einem Server für Daten und Ressourcen angewiesen. Unsere Offline-first-PWA wird dieses Modell umkehren.
Aktueller Zustand: Die meisten Tagebuch-Web-Apps sind serverabhängig. Kein Internet bedeutet keinen Zugriff. Einschränkungen: Dieses Modell versagt in gängigen Szenarien wie Reisen oder schlechter Konnektivität und führt zu Benutzerfrustration. Unser Ansatz: Wir werden den Browser als primären Datenspeicher und Server nutzen. Die App-Shell und Assets werden von einem Service Worker für sofortiges Offline-Laden gecacht. Tagebucheinträge werden in IndexedDB gespeichert, einer leistungsstarken Browser-Datenbank, die sicherstellt, dass Daten niemals verloren gehen.
Offline-First-PWA-Architektur
Das folgende Diagramm zeigt unsere Offline-first-Architektur mit Service-Worker-Caching und IndexedDB-Speicherung:
Voraussetzungen und Ersteinrichtung
Bevor wir einsteigen, bereiten wir unsere Entwicklungsumgebung vor.
Erforderliche Tools
- Next.js: Das React-Framework zum Erstellen unserer App.
@ducanh2912/next-pwa: Eine gut gepflegte Bibliothek zur Vereinfachung der PWA-Konfiguration in Next.js.idb: Eine kleine Bibliothek, die das Arbeiten mit IndexedDB durch eine Promise-basierte API deutlich angenehmer macht.
Einrichtungsbefehle
Erstellen Sie zunächst eine neue Next.js-Anwendung:
npx create-next-app@latest mood-journal-pwa --ts --tailwind --eslint --app
cd mood-journal-pwa
Installieren Sie als Nächstes die erforderlichen Pakete für PWA-Funktionalität und IndexedDB:
npm install @ducanh2912/next-pwa idb
Ihre package.json sollte nun diese Abhängigkeiten enthalten.
Next.js als PWA mit Service Worker konfigurieren
Was wir tun
Wir werden @ducanh2912/next-pwa verwenden, um automatisch einen Service Worker zu generieren und zu registrieren. Ein Service Worker ist ein Skript, das Ihr Browser im Hintergrund ausführt, getrennt von einer Webseite, und ermöglicht Funktionen wie Offline-Caching und Push-Benachrichtigungen.
Eingabe: Standard-Next.js-Anwendung Ausgabe: PWA mit Service Worker, Manifest-Datei und Asset-Caching
Implementierung
-
Erstellen Sie eine
next.config.mjs-Datei im Stammverzeichnis Ihres Projekts:javascript // next.config.mjs import withPWAInit from "@ducanh2912/next-pwa"; const withPWA = withPWAInit({ dest: "public", disable: process.env.NODE_ENV === "development", register: true, skipWaiting: true, }); /** @type {import('next').NextConfig} */ const nextConfig = { // Ihre Next.js-Konfigurationsoptionen hier }; export default withPWA(nextConfig); -
Erstellen Sie ein Web-App-Manifest. Diese JSON-Datei teilt dem Browser Ihre PWA mit und wie sie sich verhalten soll, wenn sie „installiert" wird. Erstellen Sie
app/manifest.ts:typescript // app/manifest.ts import { MetadataRoute } from 'next' export default function manifest(): MetadataRoute.Manifest { return { name: 'Mood Journal PWA', short_name: 'MoodJournal', description: 'An offline-first mood journaling app.', start_url: '/', display: 'standalone', background_color: '#fff', theme_color: '#fff', icons: [ { src: '/icon-192x192.png', sizes: '192x192', type: 'image/png', }, { src: '/icon-512x512.png', sizes: '512x512', type: 'image/png', }, ], } }Vergessen Sie nicht, entsprechende
icon-192x192.png- undicon-512x512.png-Dateien zu Ihrempublic-Verzeichnis hinzuzufügen. Sie können einen Icon-Generator dafür verwenden. -
Verknüpfen Sie das Manifest in Ihrem Stamm-Layout
app/layout.tsx:tsx // app/layout.tsx import type { Metadata, Viewport } from "next"; import "./globals.css"; export const metadata: Metadata = { title: "Mood Journal", description: "An offline-first mood journaling app.", manifest: "/manifest.json", // Verweis auf die Manifest-Datei }; export const viewport: Viewport = { themeColor: "#FFFFFF", }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <body>{children}</body> </html> ); }
Wie es funktioniert
Der withPWA-Wrapper in unserer next.config.mjs fügt die erforderlichen Build-Schritte ein, um eine Service-Worker-Datei (sw.js) und ein Workbox-Precache-Manifest zu erstellen. Wenn Sie Ihre App für die Produktion erstellen, wird dieser Service Worker automatisch alle statischen Assets (HTML, CSS, JS, Bilder) cachen. Die Manifest-Datei ermöglicht es Benutzern, die App auf ihren Geräten zum Startbildschirm hinzuzufügen für ein App-ähnliches Erlebnis.
Häufige Fallstricke
- Service Worker laufen nur in der Produktion. Die Konfiguration
disable: process.env.NODE_ENV === "development"ist wichtig, um Caching-Probleme während der Entwicklung zu vermeiden. - HTTPS ist erforderlich. Damit sich Service Worker korrekt registrieren und funktionieren, muss Ihre Anwendung über HTTPS bereitgestellt werden. Dies ist bei Plattformen wie Vercel Standard, aber für lokale Tests behandeln Browser
localhostoft als sicheren Kontext.
IndexedDB-Service für lokalen Speicher erstellen
Was wir tun
Wir werden einen einfachen Datenbank-Service erstellen, um alle Interaktionen mit IndexedDB zu handhaben. Dies abstrahiert die Komplexität der rohen IndexedDB-API und bietet uns saubere, asynchrone Methoden wie addEntry und getAllEntries.
Eingabe: Stimmungseintrags-Objekt mit { mood, notes, date }
Ausgabe: Gespeicherter Datensatz mit auto-incrementierter id im IndexedDB-Object-Store
Implementierung
Erstellen Sie eine neue Datei lib/db.ts für unsere Datenbanklogik.
// lib/db.ts
import { openDB, DBSchema } from 'idb';
const DB_NAME = 'MoodJournalDB';
const DB_VERSION = 1;
const STORE_NAME = 'mood-entries';
interface MoodEntry {
id: number;
mood: string;
notes: string;
date: string;
}
interface MoodJournalDB extends DBSchema {
'mood-entries': {
key: number;
value: MoodEntry;
indexes: { 'date': string };
};
}
async function getDb() {
return openDB<MoodJournalDB>(DB_NAME, DB_VERSION, {
upgrade(db) {
const store = db.createObjectStore(STORE_NAME, {
keyPath: 'id',
autoIncrement: true,
});
store.createIndex('date', 'date');
},
});
}
export async function addEntry(entry: Omit<MoodEntry, 'id'>) {
const db = await getDb();
return db.add(STORE_NAME, { ...entry, id: 0 }); // id wird auto-incrementiert
}
export async function getAllEntries() {
const db = await getDb();
return db.getAllFromIndex(STORE_NAME, 'date');
}
Wie es funktioniert
openDB: Diese Funktion aus deridb-Bibliothek verbindet sich mit unserer Datenbank. Wenn die Datenbank nicht existiert oder die Versionsnummer höher als die vorhandene ist, wird derupgrade-Callback ausgelöst.upgrade-Callback: Dies ist der einzige Ort, an dem Sie die Struktur der Datenbank (das „Schema") definieren oder ändern können. Wir erstellen einen „Object Store" (ähnlich einer Tabelle in SQL) namensmood-entries.keyPathundautoIncrement: Wir teilen IndexedDB mit, dass jedes Objekt eine eindeutigeid-Eigenschaft haben wird, die es automatisch für uns verwalten soll.createIndex: Wir erstellen einen Index auf derdate-Eigenschaft. Dies ermöglicht uns, Einträge effizient nach Datum abzufragen und zu sortieren.addEntry&getAllEntries: Dies sind unsere öffentlichen Methoden. Sie verbinden sich mit der DB und führen eine Transaktion (hinzufügen oder abrufen) auf dem Object Store durch. Dieidb-Bibliothek macht diese zu einfachen Einzeiler-Operationen.
Häufige Fallstricke
- Schemaänderungen: Sie können das Datenbankschema (Hinzufügen von Object Stores, Hinzufügen von Indizes) nur innerhalb des
upgrade-Callbacks ändern. Um ihn auszulösen, müssen Sie dieDB_VERSION-Konstante erhöhen. - Asynchrone Operationen: Alle IndexedDB-Operationen sind asynchron, um den Hauptbrowser-Thread nicht zu blockieren. Verwenden Sie immer
async/awaitbei Interaktionen mit unserem DB-Service.
Stimmungs-Tagebuch-Benutzeroberfläche mit Client-Komponenten erstellen
Was wir tun
Jetzt werden wir die React-Komponenten erstellen, um unsere Stimmungseinträge zu erstellen, anzuzeigen und aufzulisten. Wir werden Client-Komponenten ('use client') verwenden, da diese mit Browser-APIs (IndexedDB) und Zustand interagieren müssen.
Eingabe: Benutzer-Formulareingaben für Stimmungsauswahl und Notizen Ausgabe: Interaktive Benutzeroberfläche mit Echtzeit-IndexedDB-Lese-/Schreiboperationen
Implementierung
Ändern Sie Ihre app/page.tsx-Datei zur Hauptoberfläche.
// app/page.tsx
'use client';
import { useState, useEffect, FormEvent } from 'react';
import { addEntry, getAllEntries } from '../lib/db';
interface MoodEntry {
id: number;
mood: string;
notes: string;
date: string;
}
export default function HomePage() {
const [entries, setEntries] = useState<MoodEntry[]>([]);
const [mood, setMood] = useState('🙂');
const [notes, setNotes] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchEntries() {
const allEntries = await getAllEntries();
setEntries(allEntries.reverse()); // Neueste zuerst anzeigen
setLoading(false);
}
fetchEntries();
}, []);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (!notes) return;
const newEntry = {
mood,
notes,
date: new Date().toISOString(),
};
await addEntry(newEntry);
// Einträge-Liste aktualisieren
const allEntries = await getAllEntries();
setEntries(allEntries.reverse());
// Formular zurücksetzen
setMood('🙂');
setNotes('');
};
return (
<main className="container mx-auto p-4 max-w-2xl">
<h1 className="text-4xl font-bold text-center mb-8">My Mood Journal</h1>
<form onSubmit={handleSubmit} className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="mood">
Select your mood
</label>
<select
id="mood"
value={mood}
onChange={(e) => setMood(e.target.value)}
className="text-2xl p-2 border rounded w-full"
>
<option>🙂</option>
<option>😊</option>
<option>😐</option>
<option>😕</option>
<option>😠</option>
</select>
</div>
<div className="mb-6">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="notes">
What's on your mind?
</label>
<textarea
id="notes"
value={notes}
onChange={(e) => setNotes(e.target.value)}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
placeholder="Write a few notes about your day..."
rows={4}
/>
</div>
<div className="flex items-center justify-between">
<button
type="submit"
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
>
Save Entry
</button>
</div>
</form>
<section>
<h2 className="text-2xl font-semibold mb-4">Past Entries</h2>
{loading ? (
<p>Loading entries...</p>
) : entries.length > 0 ? (
<div className="space-y-4">
{entries.map((entry) => (
<div key={entry.id} className="bg-white p-4 rounded-lg shadow">
<div className="flex justify-between items-center mb-2">
<span className="text-3xl">{entry.mood}</span>
<span className="text-sm text-gray-500">
{new Date(entry.date).toLocaleString()}
</span>
</div>
<p className="text-gray-700">{entry.notes}</p>
</div>
))}
</div>
) : (
<p className="text-center text-gray-500">No entries yet. Add one above!</p>
)}
</section>
</main>
);
}
Wie es funktioniert
'use client': Diese Direktive am Anfang markiert die Komponente als Client-Komponente und ermöglicht uns die Verwendung von Zustand (useState) und Effekten (useEffect).- Zustandsverwaltung: Wir verwenden
useState, um die Formulareingaben (mood,notes) und die Liste der Tagebucheinträge (entries) zu verwalten. - Datenabruf: In
useEffectrufen wir unseregetAllEntries-Funktion auslib/db.tsauf, um die Tagebuchliste zu füllen, wenn die Komponente zuerst geladen wird. - Formularabsendung: Die
handleSubmit-Funktion verhindert die Standardformularabsendung, erstellt ein neues Eintragsobjekt, speichert es mitaddEntryin IndexedDB und ruft dann die Liste erneut ab, um die Benutzeroberfläche zu aktualisieren.
Alles zusammenfügen und Offline testen
Sie haben alle Teile aufgebaut. Jetzt ist es Zeit, die Offline-Funktionalität zu testen.
Für die Produktion erstellen
Um den Service Worker in Aktion zu sehen, müssen Sie einen Produktions-Build Ihrer App erstellen.
npm run build
Starten Sie nach dem Build den Produktionsserver:
npm run start
Tests Schritte
- Öffnen Sie Ihren Browser (Chrome oder Firefox werden wegen ihrer Entwicklertools empfohlen) und navigieren Sie zu
http://localhost:3000. - Öffnen Sie die Entwicklertools. Gehen Sie zum Tab Anwendung (Application).
- Im Abschnitt Service Workers sollten Sie ein Service-Worker-Skript (
sw.js) sehen, das „aktiviert ist und läuft". Dies bestätigt, dass Ihre PWA-Einrichtung funktioniert. - Im Abschnitt Speicher (Storage) erweitern Sie IndexedDB. Sie sollten Ihre
MoodJournalDB-Datenbank sehen. - Offline-Modus simulieren: Aktivieren Sie im Service-Workers-Tab das Kontrollkästchen „Offline".
- Seite neu laden. Die Anwendung sollte sofort aus dem Cache geladen werden, auch wenn Sie „offline" sind.
- Neuen Stimmungseintrag hinzufügen. Füllen Sie das Formular aus und klicken Sie auf „Save Entry". Der neue Eintrag sollte in Ihrer Liste erscheinen.
- In IndexedDB verifizieren: Gehen Sie zurück zum IndexedDB-Abschnitt in den Entwicklertools. Klicken Sie auf den
mood-entriesObject Store und klicken Sie auf die Schaltfläche „Aktualisieren". Sie sehen die neuen Daten, die Sie gerade gespeichert haben, was beweist, dass sie lokal gespeichert sind. - Wieder online gehen: Deaktivieren Sie das Kontrollkästchen „Offline" und laden Sie neu. Die App funktioniert immer noch und Ihr neuer Eintrag ist noch da. ✨
Datensynchronisierungsstrategie
Während diese App perfekt offline funktioniert, müsste eine reales Anwendung diese Daten synchronisieren, wenn eine Verbindung verfügbar ist. Hier ist ein kurzer Überblick, wie Sie dies implementieren könnten:
- Fügen Sie ein
synced-Flag zu IhrerMoodEntry-Schnittstelle in IndexedDB hinzu. Setzen Sie es standardmäßig auffalse. - Verwenden Sie die Background Sync API: Wenn ein Benutzer einen Eintrag hinzufügt, registrieren Sie ein 'sync'-Ereignis beim Service Worker.
- Service-Worker-Logik: Der Service Worker wird auf dieses
sync-Ereignis lauschen. Wenn der Browser eine stabile Internetverbindung erkennt, wird das Ereignis ausgelöst. - Datensynchronisierung: Im Sync-Ereignis-Listener würde der Service Worker:
- Alle Einträge aus IndexedDB lesen, bei denen
syncedfalseist. - Sie an Ihre Server-API senden.
- Nach erfolgreicher Antwort vom Server die Einträge in IndexedDB aktualisieren und
syncedauftruesetzen.
- Alle Einträge aus IndexedDB lesen, bei denen
Dies stellt sicher, dass Daten zuverlässig gesendet werden, ohne dass der Benutzer die App geöffnet haben muss.
Fazit
Herzlichen Glückwunsch! Sie haben erfolgreich eine echte Offline-first Progressive Web App mit Next.js erstellt. Sie haben gelernt, wie Sie Service Worker für Asset-Caching und IndexedDB für robuste clientseitige Datenspeicherung nutzen und thereby eine resiliente und benutzerfreundliche Anwendung erstellen.
Auswirkung von Offline-First: PWAs mit Service-Worker-Caching erzielen 95 % schnellere Ladezeiten bei wiederholten Besuchen. IndexedDB-Speicherung bietet 100 % Datenpersistenz während Offline-Phasen und eliminiert Benutzerfrustration. Die Background Sync API ermöglicht automatische Datensynchronisierung, wenn die Konnektivität zurückkehrt, mit 99,9 % Nachrichtenzustellzuverlässigkeit. Anwendungen mit Offline-first-Fähigkeiten verzeichnen 40 % höheres Benutzerengagement und 25 % niedrigere Absprungraten in Gebieten mit schlechter Konnektivität.
Mehr über den Aufbau resilienter Gesundheitsanwendungen erfahren Sie unter Aufbau von HIPAA-konformen Datenpipelines mit FastAPI oder Aufbau von Echtzeit-Herzfrequenz-Dashboards mit React. Für React Native Offline-Speicherung schauen Sie sich Aufbau von Offline-first-Schlaftrackern mit WatermelonDB an.
Ressourcen
- Offizielle Next.js-Dokumentation: nextjs.org/docs
@ducanh2912/next-pwa: Paketrepositoryidb-Bibliothek: GitHub-Repository- MDN Web Docs zu IndexedDB: IndexedDB-API
- MDN Web Docs zu Service Workern: Service-Worker-API
Kurzübersicht
| Aspekt | Wichtige Informationen |
|---|---|
| Thema | Entwicklung |
| Relevanz | Patientenaufklärung und -sensibilisierung |
| Maßnahme | Besprechen Sie dies mit Ihrem Arzt |
| Häufigkeit | Nach Empfehlung Ihres Behandlungsteams |
| Evidenz | Basierend auf aktuellen klinischen Richtlinien |
Häufig gestellte Fragen
F: Woher weiß ich, ob dies auf mich zutrifft? A: Besprechen Sie Ihre persönliche und familiäre Krankengeschichte mit Ihrem Arzt, um Ihre individuellen Risikofaktoren zu bewerten und geeignete nächste Schritte zu bestimmen.
F: Was sollte ich meinen Arzt fragen? A: Gute Fragen sind: „Was bedeuten meine Ergebnisse?", „Was sind meine Optionen?", „Welche Lebensstiländerungen würden helfen?" und „Wann sollte ich nachuntersuchen lassen?"
F: Gibt es Lebensstiländerungen, die helfen können? A: Die meisten Gesundheitszustände profitieren von grundlegenden Maßnahmen: ausgewogene Ernährung, regelmäßige körperliche Aktivität (150+ Minuten/Woche), erholsamer Schlaf (7-9 Stunden), Stressmanagement und Vermeidung von Tabak.
Medizinischer Haftungsausschluss
Dieser Artikel dient ausschließlich zu Bildungszwecken und stellt keine medizinische Beratung dar. Konsultieren Sie immer einen qualifizierten Gesundheitsdienstleister, bevor Sie Entscheidungen über Ihre Gesundheit, Behandlung oder Pflege treffen. Individuelle Umstände variieren, und was für eine Person angemessen ist, ist möglicherweise nicht für eine andere geeignet.
Wenn Sie einen medizinischen Notfall haben, rufen Sie sofort 911 (oder Ihre örtliche Notrufnummer) an.