Offlinedaten einfach speichern mit idb und der IndexedDB API

Ich nutze gerne die JavaScript-Bibliothek idb, einen minimalen Wrapper für die IndexedDB API. Ich habe sie schon in mehreren Web-Applikationen verwendet, um Offline-Features bereitzustellen.

Die idb-Bibliothek macht es einfach, Daten auf dem Endgerät zu speichern und sie später über ein Promise abzurufen. Sie bietet auch großartige TypeScript-Typen und -Interfaces, welche die Erstellung generischer Logik für die Interaktion mit dem Offline-Speicher unterstützen.

Eine Frau sitzt auf dem Boden, umgeben von Pappkartons. Foto: © RDNE Stock project / pexels.com

Sehen wir uns an, wie wir eine Datenbank mit idb einrichten können. Danach zeige ich euch einige Codebeispiele für wiederverwendbare generische Methoden zum Speichern und Abrufen von Daten.

Die Datenbank einrichten

Als erstes müsst ihr das Schema für die zu speichernden Daten definieren. Hier ist ein einfaches Beispiel:

import { DBSchema } from "idb"; export interface MyAwesomeDB extends DBSchema { images: { value: MyImageData; key: string; }; videos: { value: MyVideoData; key: string; }; }

Ich empfehle euch, die DB-Operationen in einer eigenen Klasse zu bündeln. Diese Klasse hat allein den Zweck, mit der Datenbank zu interagieren. Das macht euren Code einfacher zu lesen und zu verstehen. In einer Angular-Anwendung würde ich eine Serviceklasse wie diese erstellen:

import { IDBPDatabase, openDB } from "idb"; @Injectable() export class OfflineStorageService { private _databaseVersion = 1; // The promise returned when opening the IndexedDB with openDB() method. private _myDatabase$: Promise<IDBPDatabase<MyAwesomeDB>> | null = null; public init(): void { this._myDatabase$ = openDB<MyAwesomeDB>( "my-awesome-database", this._databaseVersion, { // Initialize objects in database if opened for the first time. upgrade: (database, oldVersion) => { ... }, }, ); // Handle success and error case. this._myDatabase$.then( () => console.log("IndexedDB was successfully opened.""), () => this.onOpeningDatabaseFailed(), ); } }

Daten speichern

Nehmen wir an, ihr wollt mehrere Objekte in einem bestimmten Objektspeicher der Datenbank ablegen. Dazu startet ihr eine Transaktion mit Lese- und Schreibzugriff und speichert dann die Objekte. Für diesen Vorgang könnt ihr eine generische Methode mit den Parametern storeName und items definieren:

import { StoreNames, StoreValue } from "idb"; // Stores all items in the object store named "storeName". private storeAllItemsInObjectStore<Name extends StoreNames<MyAwesomeDB>>(storeName: Name, items: StoreValue<MyAwesomeDB, Name>[]): void { this._myDatabase$?.then((database) => { const transaction = database.transaction(storeName, "readwrite"); items.forEach((item) => transaction.store.add(item)); }); }

Die Methode verwendet die Typen StoreNames und StoreValue aus der idb-Bibliothek. Diese stellen sicher, dass der an die Methode übergebene Parameter storeName tatsächlich Teil der Datenbank ist und dass die items die erwartete Datenstruktur haben. Hier ist ein Beispiel für die Verwendung dieser generischen Methode:

public storeImages(items: MyImageData[]): void { this.storeAllItemsInObjectStore("images", items); }

Daten abrufen

Beim Abrufen von Daten aus der lokalen Datenbank ziehe ich es vor, das native Promise in ein Observable umzuwandeln. Das erleichtert die Integration des Offline-Speichers in eine auf Observables basierende Application-State-Logik (z.B. NgRx store). Hier ist eine generische Methode, die ein bestimmtes Element aus einem Objektspeicher, verpackt in ein Observable, zurückgibt:

import { StoreKey, StoreNames, StoreValue } from "idb"; import { Observable, catchError, from, of, switchMap } from "rxjs"; // Retrieves the item identified via "itemKey" from the object store named "storeName". Resolves with undefined if no match is found, the database is unavailable, or there's an error. private getItemFromObjectStore<Name extends StoreNames<MyAwesomeDB>>( storeName: Name, itemKey: StoreKey<MyAwesomeDB, Name>, ): Observable<StoreValue<MyAwesomeDB, Name> | undefined> { if (this._myDatabase$ === null) { return of(undefined); } return from(this._myDatabase$).pipe( switchMap((database) => from(database.get(storeName, itemKey))), catchError((error) => { console.error(error); return of(undefined); }), ); }

Ihr könnt die generische Methode folgendermaßen verwenden:

public getImage(imageId: string): Observable<MyImageData | undefined> { return this.getItemFromObjectStore("images", imageId); }

Viel Spaß mit IndexedDB und der idb Bibliothek!

Erstellt am