</> HTML5Advent
ENFRESDEITPT

// apis

IndexedDB-Tutorial: eine Notiz-App bauen, die offline funktioniert

Ein Schritt-fuer-Schritt-Tutorial zu IndexedDB. Datenbank oeffnen, Object Store erstellen, Datensaetze hinzufuegen und lesen, nach Index abfragen, Eintraege loeschen und Versionsupgrades verwalten, alles in einer kleinen Notiz-App, die im Browser laeuft.

Eine Person steht vor verglasten Server-Racks in einem Rechenzentrum, haelt einen offenen Laptop, mit blauer Beleuchtung und Herstellerlogos auf den Glasscheiben

Der Konzeptguide erklaert, was IndexedDB ist und wann man sie verwenden sollte. Dieses Tutorial baut eine kleine Notiz-Anwendung von Grund auf, damit Sie jeden Schritt sehen: Datenbank oeffnen, Store erstellen, Datensaetze schreiben, sie zuruecklesen, nach Index abfragen und Eintraege loeschen.

Alles laeuft im Browser. Keine Build-Tools, kein Server, keine Abhaengigkeiten. Kopieren Sie jeden Block in eine einzige HTML-Datei und oeffnen Sie sie.

Schritt 1: Datenbank oeffnen

Jede IndexedDB-Interaktion beginnt mit indexedDB.open(). Sie uebergeben einen Namen und eine Versionsnummer. Falls die Datenbank noch nicht existiert oder Ihre Version hoeher ist als die auf der Festplatte, feuert der Browser upgradeneeded.

function openDB() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('notes-app', 1);

    request.onupgradeneeded = (event) => {
      const db = event.target.result;
      if (!db.objectStoreNames.contains('notes')) {
        const store = db.createObjectStore('notes', {
          keyPath: 'id',
          autoIncrement: true,
        });
        store.createIndex('by_tag', 'tag', { unique: false });
        store.createIndex('by_date', 'createdAt', { unique: false });
      }
    };

    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

Einige Dinge sind zu beachten. keyPath: 'id' sagt IndexedDB, welche Eigenschaft jedes Objekts der Primaerschluessel ist. autoIncrement: true laesst die Datenbank diesen Schluessel fuer Sie generieren. Die beiden createIndex-Aufrufe ermoeglichen es Ihnen spaeter, Notizen nach Tag oder Datum abzufragen, ohne jeden Datensatz zu durchsuchen.

Schritt 2: einen Datensatz hinzufuegen

Das Schreiben in IndexedDB erfolgt immer innerhalb einer Transaktion. Sie oeffnen eine auf dem benotigten Object Store, holen sich eine Referenz auf diesen Store und rufen add() oder put() auf.

async function addNote(db, text, tag) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('notes', 'readwrite');
    const store = tx.objectStore('notes');
    const record = {
      text,
      tag: tag || 'general',
      createdAt: new Date().toISOString(),
    };
    const request = store.add(record);
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

add() wirft einen Fehler, wenn ein Datensatz mit demselben Schluessel bereits existiert. put() ueberschreibt stattdessen. Fuer neue Datensaetze ist add() sicherer, weil es versehentliche Duplikate abfaengt.

Schritt 3: alle Datensaetze lesen

Um alles in einem Store zu lesen, oeffnen Sie eine readonly-Transaktion und rufen getAll() auf:

async function getAllNotes(db) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('notes', 'readonly');
    const store = tx.objectStore('notes');
    const request = store.getAll();
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

Wenn Sie nur einen Datensatz brauchen und seinen Schluessel kennen, verwenden Sie stattdessen store.get(key). Es gibt ein einzelnes Objekt statt eines Arrays zurueck.

Nahaufnahme eines Code-Editors mit dunklem Theme auf einem Monitor, der Ruby-Konfigurationscode mit gruener und weisser Syntaxhervorhebung zeigt, links ein Dateibaum und rechts Zeilennummern
Code schreiben in einem dunklen Editor. IndexedDB folgt demselben Request-und-Callback-Muster, das bei den meisten Browser-APIs zu finden ist, hier in Promises verpackt fuer bessere Lesbarkeit.

Schritt 4: nach Index abfragen

Indizes ermoeglichen es, Datensaetze zu finden, ohne den gesamten Store zu durchsuchen. Um alle Notizen mit dem Tag "work" zu erhalten:

async function getNotesByTag(db, tag) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('notes', 'readonly');
    const store = tx.objectStore('notes');
    const index = store.index('by_tag');
    const request = index.getAll(tag);
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

Sie koennen auch einen Key Range verwenden, um eine Spanne von Werten abzufragen. Zum Beispiel, um Notizen zu erhalten, die nach einem bestimmten Datum erstellt wurden:

const range = IDBKeyRange.lowerBound('2026-01-01T00:00:00.000Z');
const request = store.index('by_date').getAll(range);

Die vier Range-Konstruktoren sind lowerBound, upperBound, bound (beide Enden) und only (exakte Uebereinstimmung). Jeder akzeptiert einen optionalen Boolean, um den Grenzwert auszuschliessen.

Schritt 5: einen Datensatz aktualisieren

Das Aktualisieren verwendet put(). Sie muessen die Schluessel-Eigenschaft (id in unserem Schema) einschliessen, damit IndexedDB weiss, welchen Datensatz es ueberschreiben soll:

async function updateNote(db, id, newText) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('notes', 'readwrite');
    const store = tx.objectStore('notes');
    const getReq = store.get(id);
    getReq.onsuccess = () => {
      const note = getReq.result;
      if (!note) { reject(new Error('Not found')); return; }
      note.text = newText;
      const putReq = store.put(note);
      putReq.onsuccess = () => resolve();
      putReq.onerror = () => reject(putReq.error);
    };
    getReq.onerror = () => reject(getReq.error);
  });
}

Schritt 6: einen Datensatz loeschen

Rufen Sie store.delete(key) innerhalb einer readwrite-Transaktion auf:

async function deleteNote(db, id) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('notes', 'readwrite');
    const store = tx.objectStore('notes');
    const request = store.delete(id);
    request.onsuccess = () => resolve();
    request.onerror = () => reject(request.error);
  });
}

Um alle Datensaetze in einem Store zu loeschen, verwenden Sie stattdessen store.clear().

Schritt 7: Versionsupgrades verwalten

Wenn Sie einen neuen Index oder einen neuen Store hinzufuegen muessen, nachdem Benutzer bereits Version 1 haben, erhoehen Sie die Versionsnummer und behandeln Sie die Migration in onupgradeneeded:

request.onupgradeneeded = (event) => {
  const db = event.target.result;
  const oldVersion = event.oldVersion;

  if (oldVersion < 1) {
    const store = db.createObjectStore('notes', {
      keyPath: 'id',
      autoIncrement: true,
    });
    store.createIndex('by_tag', 'tag', { unique: false });
    store.createIndex('by_date', 'createdAt', { unique: false });
  }

  if (oldVersion < 2) {
    const tx = event.target.transaction;
    const store = tx.objectStore('notes');
    store.createIndex('by_priority', 'priority', { unique: false });
  }
};

Pruefen Sie event.oldVersion, um nur die Migrationen auszufuehren, die der Benutzer noch nicht gesehen hat. Ein Benutzer, der von 0 (keine Datenbank) aufsteigt, fuehrt beide Bloecke aus. Einer, der von 1 aufsteigt, fuehrt nur den zweiten aus.

Alles zusammenfuegen

Hier ist ein minimales Skript, das alle obigen Funktionen ausfuehrt:

(async () => {
  const db = await openDB();

  const id = await addNote(db, 'Buy groceries', 'personal');
  console.log('Added note with id:', id);

  const all = await getAllNotes(db);
  console.log('All notes:', all);

  const tagged = await getNotesByTag(db, 'personal');
  console.log('Personal notes:', tagged);

  await updateNote(db, id, 'Buy groceries and cook dinner');
  console.log('Updated note', id);

  await deleteNote(db, id);
  console.log('Deleted note', id);
})();

Oeffnen Sie die Browser-Konsole, um die Ausgabe zu sehen. Jede Operation ist asynchron, jeder Schreibvorgang geht durch eine Transaktion, und die Daten ueberleben Seitenaktualisierungen ohne Server.

Haeufige Fehler

FehlerWas passiertBehebung
Store ausserhalb von onupgradeneeded erstellenInvalidStateErrorAlle Schema-Aenderungen gehoeren in onupgradeneeded
await in einem Transaktions-Callback verwendenDie Transaktion schliesst sich automatisch, bevor das await aufgeloest wirdHalten Sie alle Store-Operationen synchron innerhalb eines Transaktions-Ticks, oder oeffnen Sie nach dem await eine neue Transaktion
Vergessen, die Versionsnummer zu erhoehenonupgradeneeded feuert nieErhoehen Sie die Versionsganzzahl jedes Mal, wenn Sie das Schema aendern
Einen String uebergeben, wo ein Schluessel eine Zahl erwartetDatensatz nicht gefundenPassen Sie den Typ an: Wenn autoIncrement Zahlen generiert, fragen Sie mit einer Zahl ab

IndexedDB ist ausfuehrlich, aber jedes Teil dient einem Zweck: Die Version steuert Ihr Schema, Transaktionen schuetzen Ihre Daten, und Indizes halten die Lesevorgaenge schnell. Sobald diese sieben Schritte zur Routine werden, haben Sie alles, was Sie brauchen, um strukturierte Daten auf dem Client zu speichern, sei es fuer Offline-Unterstuetzung, Caching oder einen Zustand, der die Sitzung ueberdauert.