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.
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