</> HTML5Advent
ENFRESDEITPT

// apis · Web Platform Advent #23

Was ist IndexedDB? Die Browser-Datenbank erklärt

IndexedDB ist die echte Datenbank des Browsers: asynchron, transaktional und weit größer als localStorage. Wie onupgradeneeded funktioniert, warum Transaktionen unter Ihnen zugehen, wozu Indizes da sind und wann Sie sie statt Web Storage nehmen.

Eine Wand aus hölzernen Karteikartenschubladen mit Messing-Schildhaltern und handgeschriebenen Nummern

IndexedDB ist die echte Datenbank des Browsers. Wo Web Storage Ihnen eine synchrone Kiste voller Zeichenketten mit rund 5 MB Deckel gibt, bietet IndexedDB einen asynchronen, transaktionalen Speicher für strukturierte Daten, dimensioniert in Hunderten von Megabyte und mehr. Es ist kein SQL. Es ist ein Schlüssel-Wert-Speicher mit Indizes, und sobald das sitzt, ergibt sich der Rest der API von selbst.

Eine Datenbank öffnen, und die einzige Stelle, an der man ihre Form ändert

Man öffnet eine Datenbank über Name und Versionsnummer. Ist die angeforderte Version höher als die auf der Platte, löst der Browser upgradeneeded aus, und dieser Handler ist die einzige Stelle, an der Sie Object Stores und Indizes anlegen oder löschen dürfen.

const request = indexedDB.open('notes-db', 1);

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

request.onsuccess = (event) => {
  const db = event.target.result;
};

request.onerror = () => console.error(request.error);

Das überrascht, weil es die gewohnte Praxis umdreht. Sie können einen Store nicht faul beim ersten Bedarf anlegen. Das Schema lebt in einer einzigen Funktion, gebunden an eine Versionsnummer, die Sie erhöhen, wenn sich die Struktur ändert.

Alles ist ein Request, und Requests sind asynchron

Fast jeder IndexedDB-Aufruf liefert ein IDBRequest statt eines Werts. Das Ergebnis lesen Sie in onsuccess, den Fehlerfall behandeln Sie in onerror. Nichts blockiert den Haupt-Thread, und genau das ist der Grund, sie localStorage vorzuziehen, sobald es um Menge geht.

Eine offene Schublade mit Karteikarten und Trennblättern, nummeriert von 166 bis 169. Ein IndexedDB-Index ist genau das: eine zweite Ordnung derselben Datensätze, damit man sie über etwas anderes als ihren Schlüssel findet.
Eine offene Schublade mit Karteikarten und Trennblättern, nummeriert von 166 bis 169. Ein IndexedDB-Index ist genau das: eine zweite Ordnung derselben Datensätze, damit man sie über etwas anderes als ihren Schlüssel findet.

Lesen und Schreiben geschehen in einer Transaktion

Einen Store fasst man nicht direkt an. Man öffnet eine Transaktion über einen oder mehrere Stores, im Modus readonly oder readwrite, und arbeitet durch sie hindurch.

const tx = db.transaction('notes', 'readwrite');
const store = tx.objectStore('notes');

store.add({ title: 'Buy milk', tag: 'errand', done: false });

tx.oncomplete = () => console.log('written');
tx.onerror = () => console.error(tx.error);

Hier ist die Falle, die man vorher kennen sollte. Eine Transaktion committet automatisch, sobald für sie keine Arbeit mehr aussteht. Warten Sie mittendrin auf etwas Fremdes, etwa einen fetch, schließt sich die Transaktion während des Wartens, und der nächste Aufruf wirft TransactionInactiveError. Erledigen Sie die äußere Arbeit zuerst und öffnen Sie die Transaktion danach.

Indizes, die den Namen erklären

Standardmäßig holt man Datensätze über ihren Primärschlüssel. Ein Index ist eine zweite Ordnung derselben Datensätze, mit der Sie über eine andere Eigenschaft abfragen können.

const tx = db.transaction('notes', 'readonly');
const index = tx.objectStore('notes').index('by_tag');

const req = index.getAll('errand');
req.onsuccess = () => console.log(req.result);

Für Bereiche statt exakter Treffer kombinieren Sie einen Index mit IDBKeyRange und einem Cursor, der die Ergebnisse einzeln durchläuft, statt alle in den Speicher zu laden.

Was hineinpasst, und wie viel

IndexedDB nimmt alles auf, was der Structured-Clone-Algorithmus beherrscht: Objekte, Arrays, Date, Blob, File, ArrayBuffer. Funktionen und DOM-Knoten sind nicht klonbar und werfen einen Fehler. Ein Umweg über JSON.stringify entfällt, was auch heißt, dass Zahlen Zahlen bleiben und Daten Daten.

Das Kontingent ist ein Anteil des verfügbaren Speicherplatzes und keine feste 5-MB-Grenze, also der richtige Ort für zwischengespeicherte API-Antworten, Offline-Dokumente oder Medien. Dieser Speicher ist standardmäßig best effort und kann unter Speicherdruck geräumt werden; rufen Sie navigator.storage.persist() auf, wenn die Daten das überleben müssen, und navigator.storage.estimate(), um Ihren Verbrauch zu sehen.

IndexedDB oder localStorage?

localStorageIndexedDB
API-StilSynchron, blockiert den Haupt-ThreadAsynchron, ereignisbasiert
WerteNur ZeichenkettenStrukturiert klonbare Werte
Grobe GrößeEtwa 5 MB je OriginEin Anteil des freien Speicherplatzes
AbfragenNur über den SchlüsselÜber Schlüssel, Index, Bereich mit Cursor
Gut fürTheme, Flags, kleine EinstellungenOffline-Daten, Caches, Dateien, große Listen

Beide sind auf die Origin beschränkt, und keines ist ein Ort für Geheimnisse: jedes Skript auf der Seite kann sie lesen.

Lohnt ein Wrapper?

Die rohe API ist wortreich, weil sie älter ist als Promises. Kleine Bibliotheken verpacken Requests in Promises und nehmen den Großteil des Boilerplates weg; für Anwendungscode ist das eine vernünftige Vorgabe. Lernen Sie trotzdem zuerst das Modell darunter: upgradeneeded, Transaktionen und ihr automatischer Commit verhalten sich darunter gleich, und jeder verwirrende Fehler, den Sie treffen werden, kommt aus diesen drei Punkten, nicht aus dem Wrapper.

Greifen Sie zu IndexedDB, wenn Daten strukturiert, groß oder offline verfügbar sein müssen, oft zusammen mit einem Service Worker. Behalten Sie localStorage für die Handvoll kleiner Flags, bei denen ein synchroner Lesezugriff wirklich einfacher ist.