TypeScript og IndexedDB
Denne artikel forklarer om TypeScript og IndexedDB.
Vi vil forklare TypeScript og IndexedDB med praktiske eksempler.
YouTube Video
TypeScript og IndexedDB
IndexedDB er en lav-niveau NoSQL-lagring, der gør det muligt at gemme struktureret data i browseren. Med TypeScript kan du repræsentere skemaer på en typesikker måde, hvilket reducerer fejl og forbedrer vedligeholdelsen.
Grundlæggende terminologi og arbejdsgang
IndexedDB er en lille database inde i browseren. Den administrerer data ved hjælp af mekanismer som databaser med navne og versioner, objektlagre, transaktioner, indekser og markører. Databaser har versioner, og når versionen opgraderes, kaldes onupgradeneeded for at opdatere skemaet, f.eks. ved at oprette eller ændre tabeller.
Åbning af IndexedDB (grundlæggende mønster)
Først viser vi et eksempel på, hvordan man åbner en database med IndexedDB og opretter en object store i onupgradeneeded, hvis nødvendigt.
1// Open an IndexedDB database and create an object store if needed.
2// This code shows the classic callback-based IndexedDB API wrapped into a Promise.
3function openDatabase(dbName: string, version: number): Promise<IDBDatabase> {
4 return new Promise((resolve, reject) => {
5 const request = indexedDB.open(dbName, version);
6
7 request.onerror = () => {
8 reject(request.error);
9 };
10
11 request.onupgradeneeded = (event) => {
12 const db = request.result;
13 if (!db.objectStoreNames.contains('todos')) {
14 // Create object store with keyPath 'id'
15 db.createObjectStore('todos', { keyPath: 'id' });
16 }
17 };
18
19 request.onsuccess = () => {
20 resolve(request.result);
21 };
22 });
23}
24
25// Usage example:
26openDatabase('my-db', 1)
27 .then(db => {
28 console.log('DB opened', db.name, db.version);
29 db.close();
30 })
31 .catch(err => console.error('Failed to open DB', err));- Denne kode åbner databasen og logger, om det lykkedes eller mislykkedes.
- Hvis nødvendigt, oprettes
todos-objektlageret ionupgradeneeded.
Definering af typer i TypeScript (modeller)
Dernæst definerer vi datatyper ved hjælp af TypeScript. Dette sikrer typesikkerhed i de efterfølgende CRUD-operationer.
1// Define the TypeScript interface for a Todo item.
2// This helps with type safety in the database operations.
3interface Todo {
4 id: string; // primary key
5 title: string;
6 completed: boolean;
7 createdAt: number;
8}- Her definerer vi typen
Todo.
Enkle eksempler på CRUD-funktionsimplementering
Næste viser vi grundlæggende CRUD-operationer såsom at tilføje, hente, opdatere og slette fra object store. Hver funktion tager et IDBDatabase-objekt og returnerer et Promise.
1// CRUD utilities for the 'todos' object store.
2// Each operation uses a transaction and returns a Promise for easier async/await usage.
3
4function addTodo(db: IDBDatabase, todo: Todo): Promise<void> {
5 return new Promise((resolve, reject) => {
6 const tx = db.transaction('todos', 'readwrite');
7 const store = tx.objectStore('todos');
8 const req = store.add(todo);
9
10 req.onsuccess = () => {
11 console.log('Todo added', todo.id);
12 };
13 req.onerror = () => reject(req.error);
14
15 tx.oncomplete = () => resolve();
16 tx.onerror = () => reject(tx.error);
17 });
18}- Denne funktion tilføjer en ny
Todotiltodos-lageret i IndexedDB. Den returnerer et Promise for asynkron håndtering, som bliver resolved, når behandlingen er færdig.
1function getTodo(db: IDBDatabase, id: string): Promise<Todo | undefined> {
2 return new Promise((resolve, reject) => {
3 const tx = db.transaction('todos', 'readonly');
4 const store = tx.objectStore('todos');
5 const req = store.get(id);
6
7 req.onsuccess = () => resolve(req.result as Todo | undefined);
8 req.onerror = () => reject(req.error);
9 });
10}- Denne funktion henter
Todoen med det angivne ID og returnerer objektet, hvis det findes. Hvis der ikke findes nogen matchende data, returnerer denundefined.
1function updateTodo(db: IDBDatabase, todo: Todo): Promise<void> {
2 return new Promise((resolve, reject) => {
3 const tx = db.transaction('todos', 'readwrite');
4 const store = tx.objectStore('todos');
5 const req = store.put(todo);
6
7 req.onsuccess = () => {
8 console.log('Todo updated', todo.id);
9 };
10 req.onerror = () => reject(req.error);
11
12 tx.oncomplete = () => resolve();
13 tx.onerror = () => reject(tx.error);
14 });
15}- Denne funktion opdaterer eksisterende
Todo-data. Ved succes logges ID'et på den opdateredeTodo.
1function deleteTodo(db: IDBDatabase, id: string): Promise<void> {
2 return new Promise((resolve, reject) => {
3 const tx = db.transaction('todos', 'readwrite');
4 const store = tx.objectStore('todos');
5 const req = store.delete(id);
6
7 req.onsuccess = () => {
8 console.log('Todo deleted', id);
9 };
10 req.onerror = () => reject(req.error);
11
12 tx.oncomplete = () => resolve();
13 tx.onerror = () => reject(tx.error);
14 });
15}-
Denne funktion sletter
Todoen med det angivne ID. Når behandlingen er succesfuld, logges det slettede ID. -
Disse funktioner løser eller afviser et Promise afhængigt af transaktionsafslutning eller fejl. Inkludering af output via
console.loggør det nemt at følge, hvad der sker under eksekveringen.
Indekser og sammensatte forespørgsler
Ved at bruge indekser i IndexedDB kan du effektivt søge på bestemte felter. Her opretter vi et indeks for createdAt og giver et eksempel på en intervalforespørgsel.
1// When opening DB, create an index for createdAt.
2// Then demonstrate a range query using the index.
3
4function openDatabaseWithIndex(dbName: string, version: number): Promise<IDBDatabase> {
5 return new Promise((resolve, reject) => {
6 const request = indexedDB.open(dbName, version);
7
8 request.onupgradeneeded = () => {
9 const db = request.result;
10 if (!db.objectStoreNames.contains('todos')) {
11 const store = db.createObjectStore('todos', { keyPath: 'id' });
12 // Create an index on createdAt for sorting/filtering
13 store.createIndex('by-createdAt', 'createdAt', { unique: false });
14 } else {
15 const store = request.transaction!.objectStore('todos');
16 if (!store.indexNames.contains('by-createdAt')) {
17 store.createIndex('by-createdAt', 'createdAt', { unique: false });
18 }
19 }
20 };
21
22 request.onerror = () => reject(request.error);
23 request.onsuccess = () => resolve(request.result);
24 });
25}- Denne funktion åbner databasen og opretter eller verificerer
by-createdAtindekset påcreatedAt-feltet. Dette muliggør effektiv søgning og sortering efter oprettelsesdato.
1async function getTodosCreatedAfter(db: IDBDatabase, timestamp: number): Promise<Todo[]> {
2 return new Promise((resolve, reject) => {
3 const tx = db.transaction('todos', 'readonly');
4 const store = tx.objectStore('todos');
5 const index = store.index('by-createdAt');
6 const range = IDBKeyRange.lowerBound(timestamp, true); // exclusive
7 const req = index.openCursor(range);
8
9 const results: Todo[] = [];
10 req.onsuccess = (event) => {
11 const cursor = (event.target as IDBRequest).result as IDBCursorWithValue | null;
12 if (cursor) {
13 results.push(cursor.value as Todo);
14 cursor.continue();
15 } else {
16 resolve(results);
17 }
18 };
19 req.onerror = () => reject(req.error);
20 });
21}-
Denne funktion henter kun
Todos, der er oprettet efter det angivne tidsstempel. Ved at bruge indekset muliggøres effektiv datasøgning i rækkefølge efter oprettelsesdato. -
I dette eksempel oprettes et
by-createdAtindeks under databaseopgraderingen, ogTodo-elementer oprettet efter det angivne tidspunkt gennemgås med en markør.
Promise-baseret letvægtsindpakning
Den lav-niveau IndexedDB API er kompleks at skrive, og gentagne lignende operationer kan føre til overflødighed og fejl. Derfor, ved at forberede en generisk TypeScript wrapper-klasse, der abstraherer operationerne, forbedrer vi kodens enkelhed og vedligeholdelse. Nedenfor er en implementering med fokus på grundlæggende funktionalitet.
1// A minimal TypeScript wrapper around IndexedDB to simplify common operations.
2// This class is generic over the store's value type and assumes 'keyPath' is 'id'.
3
4class IDBWrapper<T extends { id: string }> {
5 private dbPromise: Promise<IDBDatabase>;
6
7 constructor(private dbName: string, private version: number, private storeName: string) {
8 this.dbPromise = this.open();
9 }- Denne klasse wrapper IndexedDB-operationer og giver typesikre CRUD-metoder. Den antager en object store, hvor nøglen er
id.
1 private open(): Promise<IDBDatabase> {
2 return new Promise((resolve, reject) => {
3 const req = indexedDB.open(this.dbName, this.version);
4 req.onerror = () => reject(req.error);
5 req.onupgradeneeded = () => {
6 const db = req.result;
7 if (!db.objectStoreNames.contains(this.storeName)) {
8 db.createObjectStore(this.storeName, { keyPath: 'id' });
9 }
10 };
11 req.onsuccess = () => resolve(req.result);
12 });
13 }- Den åbner databasen og opretter en ny object store, hvis nødvendigt. Initialisering af lageret udføres ved hjælp af upgrade-begivenheden.
1 async add(item: T): Promise<void> {
2 const db = await this.dbPromise;
3 await new Promise<void>((resolve, reject) => {
4 const tx = db.transaction(this.storeName, 'readwrite');
5 const store = tx.objectStore(this.storeName);
6 const req = store.add(item);
7 req.onsuccess = () => {
8 console.log('added', item.id);
9 };
10 req.onerror = () => reject(req.error);
11 tx.oncomplete = () => resolve();
12 tx.onerror = () => reject(tx.error);
13 });
14 }- Tilføjer data til IndexedDB-lageret. Efter tilføjelse logges ID'et til konsollen.
1 async get(id: string): Promise<T | undefined> {
2 const db = await this.dbPromise;
3 return new Promise((resolve, reject) => {
4 const tx = db.transaction(this.storeName, 'readonly');
5 const store = tx.objectStore(this.storeName);
6 const req = store.get(id);
7 req.onsuccess = () => resolve(req.result as T | undefined);
8 req.onerror = () => reject(req.error);
9 });
10 }- Henter data, der svarer til det angivne ID.
undefinedreturneres, hvis dataene ikke eksisterer.
1 async put(item: T): Promise<void> {
2 const db = await this.dbPromise;
3 return new Promise((resolve, reject) => {
4 const tx = db.transaction(this.storeName, 'readwrite');
5 const store = tx.objectStore(this.storeName);
6 const req = store.put(item);
7 req.onsuccess = () => {
8 console.log('put', item.id);
9 };
10 req.onerror = () => reject(req.error);
11 tx.oncomplete = () => resolve();
12 tx.onerror = () => reject(tx.error);
13 });
14 }- Opdaterer eksisterende data eller tilføjer nye data. Efter behandlingen logges det opdaterede ID.
1 async delete(id: string): Promise<void> {
2 const db = await this.dbPromise;
3 return new Promise((resolve, reject) => {
4 const tx = db.transaction(this.storeName, 'readwrite');
5 const store = tx.objectStore(this.storeName);
6 const req = store.delete(id);
7 req.onsuccess = () => {
8 console.log('deleted', id);
9 };
10 req.onerror = () => reject(req.error);
11 tx.oncomplete = () => resolve();
12 tx.onerror = () => reject(tx.error);
13 });
14 }- Sletter dataene med det angivne ID. Ved succes logges det slettede ID.
1 async getAll(): Promise<T[]> {
2 const db = await this.dbPromise;
3 return new Promise((resolve, reject) => {
4 const tx = db.transaction(this.storeName, 'readonly');
5 const store = tx.objectStore(this.storeName);
6 const req = store.getAll();
7 req.onsuccess = () => resolve(req.result as T[]);
8 req.onerror = () => reject(req.error);
9 });
10 }
11}- Henter alle data i lageret. Returværdien er et array af typen
T.
1// Example usage with Todo type:
2interface Todo {
3 id: string;
4 title: string;
5 completed: boolean;
6 createdAt: number;
7}
8
9const todoStore = new IDBWrapper<Todo>('my-db', 1, 'todos');
10
11(async () => {
12 const newTodo: Todo = { id: '1', title: 'Learn IndexedDB', completed: false, createdAt: Date.now() };
13 await todoStore.add(newTodo);
14 const fetched = await todoStore.get('1');
15 console.log('fetched', fetched);
16 newTodo.completed = true;
17 await todoStore.put(newTodo);
18 const all = await todoStore.getAll();
19 console.log('all todos', all);
20 await todoStore.delete('1');
21})();-
Denne kode er et faktisk brugs eksempel på
IDBWrapper-klassen. Dette viser processen for at tilføje, hente, opdatere, liste og sletteTodo-data. -
Denne wrapper muliggør simpel håndtering af grundlæggende CRUD-operationer. I et virkeligt miljø skal du også håndtere fejlhåndtering og skemastyring (indekser).
Skemamigrering (versionsopgradering)
For at ændre databaseskemaet skal du øge det andet argument (version) af indexedDB.open og opdatere det i onupgradeneeded. Du skal designe det, så eksisterende transaktioner er afsluttet, og ødelæggende ændringer undgås.
1// Example of handling upgrade to version 2: add an index and perhaps migrate data.
2// onupgradeneeded receives an event where oldVersion and newVersion are accessible.
3
4function upgradeToV2(dbName: string): Promise<IDBDatabase> {
5 return new Promise((resolve, reject) => {
6 const req = indexedDB.open(dbName, 2);
7 req.onupgradeneeded = (ev) => {
8 const db = req.result;
9 const oldVersion = (ev as IDBVersionChangeEvent).oldVersion;
10 console.log('Upgrading DB from', oldVersion, 'to', db.version);
11 let store: IDBObjectStore;
12 if (!db.objectStoreNames.contains('todos')) {
13 store = db.createObjectStore('todos', { keyPath: 'id' });
14 } else {
15 store = req.transaction!.objectStore('todos');
16 }
17 // Add index if not present
18 if (!store.indexNames.contains('by-completed')) {
19 store.createIndex('by-completed', 'completed', { unique: false });
20 }
21
22 // Optional: data migration logic if necessary can go here,
23 // but heavy migrations often should be done lazily on read.
24 };
25 req.onsuccess = () => resolve(req.result);
26 req.onerror = () => reject(req.error);
27 });
28}- Tung behandling inde i
onupgradeneededkan blokere UI'et, så hold det på et minimum, og overvej forsinket migration (scenebehandling under app-opstart) hvis muligt.
Forsigtighed ved transaktioner (livscyklus og fejl)
Transaktioner bliver automatisk committed, før eksekveringen af scriptet, der oprettede dem, slutter. Når du bruger await i en transaktion, kan den blive committed uventet; vær opmærksom på dette, når du udfører flere asynkrone operationer i samme transaktion.
1// Bad pattern: awaiting outside transaction callbacks can cause tx to auto-commit.
2// Good pattern is to chain requests and resolve on tx.oncomplete as shown earlier.
3
4// Example: Do multiple operations inside single tx, avoid awaiting inside.
5function multiOperation(db: IDBDatabase, items: Todo[]): Promise<void> {
6 return new Promise((resolve, reject) => {
7 const tx = db.transaction('todos', 'readwrite');
8 const store = tx.objectStore('todos');
9
10 for (const item of items) {
11 const req = store.put(item);
12 req.onerror = () => console.error('put error', req.error);
13 // Do NOT await here; just schedule requests synchronously.
14 }
15
16 tx.oncomplete = () => {
17 console.log('All operations in transaction completed');
18 resolve();
19 };
20 tx.onerror = () => reject(tx.error);
21 });
22}- Vær opmærksom på transaktionens levetid; brug separate transaktioner om nødvendigt, eller planlæg operationer synkront inden for en transaktion.
Cursor-applikationer og paginering
Ved at bruge en markør kan du behandle store mængder data sekventielt eller implementere simpel paginering uden at bruge offset.
1// Example: fetch first N items using a cursor (ascending by key).
2function fetchFirstN(db: IDBDatabase, n: number): Promise<Todo[]> {
3 return new Promise((resolve, reject) => {
4 const tx = db.transaction('todos', 'readonly');
5 const store = tx.objectStore('todos');
6 const req = store.openCursor();
7 const out: Todo[] = [];
8 req.onsuccess = (ev) => {
9 const cursor = (ev.target as IDBRequest).result as IDBCursorWithValue | null;
10 if (cursor && out.length < n) {
11 out.push(cursor.value as Todo);
12 cursor.continue();
13 } else {
14 resolve(out);
15 }
16 };
17 req.onerror = () => reject(req.error);
18 });
19}- Ved at hente elementer ét ad gangen med cursors kan hukommelsesforbruget reduceres. Når du implementerer paginering, er det almindeligt at huske den sidst læste nøgle.
Fejlhåndtering og fallback
1// Feature detection
2if (!('indexedDB' in window)) {
3 console.warn('IndexedDB is not supported. Falling back to localStorage.');
4 // implement fallback logic...
5}- IndexedDB er muligvis ikke tilgængelig på grund af implementeringsforskelle mellem browsere eller brugernes privatlivsindstillinger, såsom privat browsing. Kontroller derfor, om
indexedDBfindes, og giv i modsat fald en fallback såsomlocalStorage.
Ydeevne og bedste praksis
IndexedDB er hurtig og kraftfuld, men ydeevnen kan variere meget afhængigt af designet og hvordan data håndteres. Afhængigt af anvendelsestilfældet kan optimering udføres på følgende måder:.
- Design objektlageret i henhold til den faktiske brug. For eksempel, hvis der er mange læsninger, skal du oprette indekser; hvis der er mange skrivninger, bør nøgle-designet holdes simpelt.
- Store binære data såsom billeder og lyd bør gemmes som blobs, eller administreres ved hjælp af File API eller service workers, hvis det er nødvendigt. Komprimering kan også overvejes, hvis det er nødvendigt.
- Hold transaktioner så korte som muligt og udfør tung behandling uden for transaktionen for at minimere låsetiden.
- Indekser kan fremskynde søgninger, men gøre indsættelser og opdateringer langsommere, så opret kun dem, der virkelig er nødvendige.
- Når du arbejder med mange små datastykker, kan det udpine hukommelsen at hente dem alle på én gang med
getAll(). Du kan reducere hukommelsesforbruget ved at opdele behandlingen med markører.
Sikkerhed og privatliv
IndexedDB-data er isoleret pr. domæne og protokol i henhold til same-origin-politikken. Design ud fra antagelsen om, at data kan gå tabt, hvis brugere sletter browserdata eller bruger privat tilstand.
Sammenfatning og anbefalede designmønstre
For at bruge IndexedDB effektivt med TypeScript er det vigtigt at forberede typer og asynkrone processer, være opmærksom på versionsstyring og transaktionsdesign samt indpakke fælles processer for at forbedre vedligeholdelsen.
- At definere typer i TypeScript og wrappe IndexedDB-operationer med Promise/async/await forbedrer sikkerhed og læsbarhed af koden.
- Skemachanges bør bruge versionsstyring med
onupgradeneeded, og tung behandling bør udskydes, hvor det er muligt. - Design transaktioner til at være korte og undgå tung asynkron behandling inden for samme transaktion.
- Ved at oprette wrapper-klasser kan du reducere overflødige fælles processer såsom fejlhåndtering, logning og typedefinitioner.
Du kan følge med i ovenstående artikel ved hjælp af Visual Studio Code på vores YouTube-kanal. Husk også at tjekke YouTube-kanalen.