TypeScript og IndexedDB
Denne artikkelen forklarer om TypeScript og IndexedDB.
Vi skal forklare TypeScript og IndexedDB med praktiske eksempler.
YouTube Video
TypeScript og IndexedDB
IndexedDB er en lavnivå NoSQL-lagring som lar deg lagre strukturert data i nettleseren. Med TypeScript kan du representere skjemaer på en typesikker måte, som reduserer feil og forbedrer vedlikeholdbarheten.
Grunnleggende terminologi og arbeidsflyt
IndexedDB er en liten database inne i nettleseren. Den håndterer data ved hjelp av mekanismer som databaser med navn og versjoner, objektlagring, transaksjoner, indekser og markører (cursors). Databaser har versjoner, og når versjonen oppgraderes, blir onupgradeneeded kalt for å oppdatere skjemaet, som for eksempel å opprette eller endre tabeller.
Åpning av IndexedDB (grunnleggende mønster)
Først viser vi et eksempel på hvordan man åpner en database med IndexedDB og oppretter en object store i onupgradeneeded om nødvendig.
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 koden åpner databasen og logger om det lykkes eller mislyktes.
- Om nødvendig opprettes
todos-store ionupgradeneeded.
Definere typer i TypeScript (Modeller)
Deretter definerer vi datatyper med TypeScript. Dette sikrer typesikkerhet i senere CRUD-operasjoner.
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-funksjonsimplementering
Deretter viser vi grunnleggende CRUD-operasjoner som å legge til, hente, oppdatere og slette fra object store. Hver funksjon tar en IDBDatabase 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 funksjonen legger til en ny
Todoitodos-lageret i IndexedDB. Den returnerer et Promise for asynkron håndtering, som løses når behandlingen er ferdig.
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 funksjonen henter
Todomed spesifisert ID og returnerer objektet hvis det finnes. Dersom ingen matchende data finnes, returneresundefined.
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 funksjonen oppdaterer eksisterende
Todo-data. Ved suksess logges ID-en til den oppdaterteTodo.
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 funksjonen sletter
Todomed spesifisert ID. Når behandlingen lykkes, logges den slettede ID-en. -
Disse funksjonene løser eller avslår et Promise avhengig av om transaksjonen fullføres eller feiler. Hvis du inkluderer output via
console.log, blir det enkelt å spore hva som skjer under kjøringen.
Indekser og sammensatte spørringer
Ved å bruke indekser i IndexedDB kan du søke effektivt i spesifikke felt. Her lager vi en indeks for createdAt og gir et eksempel på en intervallspørring.
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 funksjonen åpner databasen og oppretter eller verifiserer
by-createdAt-indeksen på feltetcreatedAt. Dette muliggjør effektivt søk og sortering etter opprettelsesdato.
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 funksjonen henter kun
Todossom er opprettet etter den angitte tidsstempelet. Ved å bruke indeksen kan man effektivt skanne data i rekkefølge etter opprettelsesdato. -
I dette eksempelet opprettes en
by-createdAt-indeks under databaseoppgraderingen, ogTodo-elementer som er opprettet etter angitt tid listes med en markør (cursor).
Promise-basert lettvekts wrapper
Den lavnivå IndexedDB API-en er kompleks å skrive, og gjentatte like operasjoner kan føre til overflødigheter og feil. Derfor, ved å lage en generisk TypeScript-wrapperklasse som abstraherer operasjonene, forbedrer vi kodeforenkling og vedlikeholdbarhet. Under er en implementasjon som fokuserer på grunnleggende funksjonalitet.
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 klassen wrapper IndexedDB-operasjoner og gir typesikre CRUD-metoder. Den antar et object store hvor nøkkelen 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 åpner databasen og oppretter en ny object store om nødvendig. Initialisering av store utføres med upgrade-eventen.
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 }- Legger til data i IndexedDB-store. Etter tillegg logges ID-en 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 tilsvarende spesifisert ID.
undefinedreturneres hvis dataene ikke finnes.
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 }- Oppdaterer eksisterende data eller legger til nye data. Etter behandling logges den oppdaterte ID-en.
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 spesifisert ID. Ved suksess logges den slettede ID-en.
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 all data i store. Returverdien er et array av type
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 koden er et faktisk brukseksempel på klassen
IDBWrapper. Dette viser flyten for å legge til, hente, oppdatere, liste og sletteTodo-data. -
Denne wrapperen tillater enkel håndtering av grunnleggende CRUD-operasjoner. I et virkelig miljø må du også håndtere feilhåndtering og skjemaadministrasjon (indekser).
Skjemamigrering (versjonsoppgradering)
For å endre databaseskjemaet, øk det andre argumentet (versjon) til indexedDB.open og oppdater det i onupgradeneeded. Du må designe det slik at eksisterende transaksjoner er ferdige og destruktive endringer unngå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}- Kraftig behandling inne i
onupgradeneededkan blokkere brukergrensesnittet, så hold det til et minimum, og vurder forsinket migrering (midlertidig behandling under app-oppstart) hvis mulig.
Forsiktighetsregler om transaksjoner (livssyklus og feil)
Transaksjoner forpliktes automatisk før utførelsen av skriptet som opprettet dem, avsluttes. Når du bruker await inne i en transaksjon, kan den fullføres uventet; vær oppmerksom ved flere asynkrone operasjoner i samme transaksjon.
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 oppmerksom på transaksjonens levetid; bruk separate transaksjoner ved behov, eller utfør operasjoner synkront innenfor en transaksjon.
Cursor-applikasjoner og paginering
Ved å bruke en peker (cursor) kan du behandle store mengder data sekvensielt eller implementere enkel paginering uten å bruke forskyvninger.
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 å hente elementer én etter én med cursorer kan minnebruken reduseres. Når du implementerer paginering, er det vanlig å huske den sist leste nøkkelen.
Feilhå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 kan være utilgjengelig på grunn av implementasjonsforskjeller mellom nettlesere eller brukernes personverninnstillinger, som privat nettlesing. Derfor bør du sjekke om
indexedDBfinnes, og hvis ikke, tilby et alternativ somlocalStorage.
Ytelse og beste praksis
IndexedDB er rask og kraftig, men ytelsen kan variere mye avhengig av designet og hvordan dataene håndteres. Avhengig av bruksområdet kan optimalisering gjøres på følgende måter:.
- Design objektlageret i henhold til faktisk bruk. For eksempel, hvis det er mange lesinger, lag indekser; hvis det er mange skrivinger, hold nøkkeldesignet enkelt.
- Store binærdata som bilder og lyd bør lagres som Blobs, eller håndteres ved hjelp av File API eller service workers om nødvendig. Kompresjon kan også vurderes hvis det er nødvendig.
- Hold transaksjoner så korte som mulig, og utfør tung behandling utenfor transaksjonen for å minimere låsetiden.
- Indekser kan gjøre søk raskere, men kan redusere hastigheten på innsettinger og oppdateringer, så opprett kun de som virkelig er nødvendige.
- Når du arbeider med mange små databiter, kan det å hente alle samtidig med
getAll()bruke opp minnet. Du kan redusere minnebruken ved å dele opp behandlingen med markører (cursors).
Sikkerhet og personvern
IndexedDB-data er isolert per domene og protokoll i henhold til same-origin policy. Design med forutsetningen om at data kan gå tapt hvis brukere sletter nettleserdata eller bruker privat modus.
Oppsummering og anbefalte designmønstre
For å bruke IndexedDB effektivt med TypeScript, er det viktig å forberede typer og asynkrone prosesser, være oppmerksom på versjonshåndtering og transaksjonsdesign, samt å pakke inn felles prosesser for å forbedre vedlikeholdbarhet.
- Å definere typer i TypeScript og wrappe IndexedDB-operasjoner med Promise/async/await forbedrer sikkerhet og lesbarhet i koden.
- Skjemaendringer bør bruke versjonsstyring med
onupgradeneeded, og tung behandling bør forsinkes når mulig. - Design transaksjoner slik at de er korte, og unngå tung asynkron behandling innenfor samme transaksjon.
- Ved å lage wrapper-klasser kan du redusere overflødige prosesser som feilbehandling, logging og typedefinisjoner.
Du kan følge med på artikkelen ovenfor ved å bruke Visual Studio Code på vår YouTube-kanal. Vennligst sjekk ut YouTube-kanalen.