TypeScript och IndexedDB
Denna artikel förklarar TypeScript och IndexedDB.
Vi kommer att förklara TypeScript och IndexedDB med praktiska exempel.
YouTube Video
TypeScript och IndexedDB
IndexedDB är en låg-nivå NoSQL-lagring som låter dig spara strukturerad data i webbläsaren. Med TypeScript kan du representera scheman på ett typesäkert sätt, vilket minskar fel och förbättrar underhållbarheten.
Grundläggande terminologi och arbetsflöde
IndexedDB är en liten databas i webbläsaren. Den hanterar data med hjälp av mekanismer såsom databaser med namn och versioner, objektlager, transaktioner, index och markörer. Databaser har versioner, och när versionen uppgraderas anropas onupgradeneeded för att uppdatera schemat, t.ex. genom att skapa eller ändra tabeller.
Öppna IndexedDB (grundläggande mönster)
Först visar vi ett exempel på att öppna en databas med IndexedDB och skapa ett objektlager i onupgradeneeded om det behövs.
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));- Denna kod öppnar databasen och loggar om det lyckades eller misslyckades.
- Om det behövs skapas
todos-lagret ionupgradeneeded.
Definiera typer i TypeScript (Modeller)
Nästa steg är att definiera datatyper med TypeScript. Detta säkerställer typesäkerhet i efterföljande 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}- Här definierar vi typen
Todo.
Exempel på enkla CRUD-funktionsimplementationer
Därefter visar vi grundläggande CRUD-operationer som att lägga till, hämta, uppdatera och ta bort från objektlagret. Varje funktion tar emot en IDBDatabase och returnerar ett 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}- Denna funktion lägger till en ny
Todoitodos-lagret i IndexedDB. Den returnerar ett Promise för asynkron hantering, som löses när bearbetningen är klar.
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}- Denna funktion hämtar
Todomed det angivna ID:t och returnerar objektet om det hittas. Om ingen matchande data hittas returnerasundefined.
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}- Denna funktion uppdaterar befintlig
Todo-data. Vid framgång loggas ID:t för den uppdateradeTodo.
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}-
Denna funktion tar bort
Todomed det angivna ID:t. När processen lyckas loggas det borttagna ID:t. -
Dessa funktioner löser eller avslår ett Promise beroende på om transaktionen slutförs eller om det uppstår fel. Att inkludera utdata via
console.loggör det enkelt att följa vad som händer under körningen.
Index och sammansatta frågor
Genom att använda index i IndexedDB kan du effektivt söka på specifika fält. Här skapar vi ett index för createdAt och ger ett exempel på en intervallförfrågan.
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}- Denna funktion öppnar databasen och skapar eller verifierar indexet
by-createdAtpå fältetcreatedAt. Detta möjliggör effektiv sökning och sortering efter skapelsedatum.
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}-
Denna funktion hämtar endast
Todossom skapades efter den angivna tidsstämpeln. Genom att använda indexet kan data skannas effektivt i skapelsedatumsordning. -
I detta exempel skapas ett
by-createdAt-index vid databasuppgraderingen, ochTodo-poster som skapats efter en viss tid räknas upp med hjälp av en markör.
Lättviktigt wrapper-bibliotek baserat på Promise
Den låg-nivå IndexedDB-API:n är komplicerad att skriva och upprepade liknande operationer kan leda till redundans och buggar. Därför, genom att förbereda en generell TypeScript-omslutningsklass som abstraherar operationerna, förbättrar vi kodens enkelhet och underhållbarhet. Nedan är en implementation som är inriktad på grundläggande 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 }- Denna klass kapslar in IndexedDB-operationer och tillhandahåller typesäkra CRUD-metoder. Den antar ett objektlager där nyckeln är
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 öppnar databasen och skapar ett nytt objektlager om det behövs. Lagerinitiering utförs med hjälp av upgrade-händelsen.
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 }- Lägger till data i IndexedDB-lagret. Efter tillägg loggas ID:t till konsolen.
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 }- Hämtar data som motsvarar det angivna ID:t.
undefinedreturneras om data inte finns.
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 }- Uppdaterar befintliga data eller lägger till nya data. Efter processen loggas det uppdaterade ID:t.
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 }- Tar bort data med det specifika ID:t. Vid framgång loggas det borttagna ID:t.
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}- Hämtar all data i lagret. Returnerat värde är en array av 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})();-
Denna kod är ett faktisk användningsexempel på klassen
IDBWrapper. Detta visar flödet för att lägga till, hämta, uppdatera, lista och ta bortTodo-data. -
Denna wrapper möjliggör enkel hantering av grundläggande CRUD-operationer. I en verklig miljö måste man även hantera felhantering och schemavalidering (index).
Schema-migrering (versionsuppgradering)
För att ändra databasens schema, öka det andra argumentet (version) för indexedDB.open och uppdatera det i onupgradeneeded. Du måste utforma det så att befintliga transaktioner har avslutats och destruktiva ändringar undviks.
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 bearbetning inne i
onupgradeneededkan blockera gränssnittet, så håll det till ett minimum, och överväg fördröjd migration (stagingbehandling vid app-start) om möjligt.
Varningar kring transaktioner (livscykel och fel)
Transaktioner skrivs automatiskt innan skriptet som skapade dem har avslutats. När du använder await inne i en transaktion kan den oväntat kommitteras; var försiktig vid flera asynkrona operationer inom samma 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}- Var medveten om transaktionens livslängd; använd separata transaktioner om det behövs, eller schemalägg operationer synkront inom en transaktion.
Användning av cursorer och paginering
Genom att använda en kursör kan du bearbeta storskaliga data sekventiellt eller implementera enkel paginering utan att använda 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}- Genom att hämta poster en efter en med cursorer kan minnesanvändningen minskas. Vid implementering av paginering är det vanligt att komma ihåg den senast lästa nyckeln.
Felkorrigering och reservlösning
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 vara otillgängligt på grund av implementeringsskillnader mellan webbläsare eller användarnas sekretessinställningar, såsom privat surfning. Kontrollera därför om
indexedDBfinns, och om inte, tillhandahåll ett alternativ såsomlocalStorage.
Prestanda och bästa praxis
IndexedDB är snabb och kraftfull, men prestandan kan variera kraftigt beroende på dess design och hur data hanteras. Beroende på användningsfall kan optimering göras på följande sätt:.
- Designa objektlagret utifrån faktisk användning. Till exempel, om det är många läsningar, tillhandahåll index; om det är många skrivningar, håll nyckeldesignen enkel.
- Stora binära data, som bilder och ljud, bör lagras som Blob:ar eller hanteras med File API eller service workers om det behövs. Komprimering kan också övervägas om det är nödvändigt.
- Håll transaktioner så korta som möjligt och utför tunga beräkningar utanför transaktionen för att minimera låstid.
- Index kan snabba upp sökningar, men göra insättningar och uppdateringar långsammare, så skapa endast dem som verkligen behövs.
- Vid hantering av många små datadelar kan hämtning av alla samtidigt med
getAll()göra slut på minnet. Du kan minska minnesanvändningen genom att dela upp bearbetningen med markörer.
Säkerhet och integritet
IndexedDB-data är isolerade per domän och protokoll enligt samma ursprungsprincip (same-origin policy). Designa med antagandet att data kan gå förlorad om användare tar bort webbläsardata eller använder privat läge.
Sammanfattning och rekommenderade designmönster
För att använda IndexedDB effektivt med TypeScript är det viktigt att förbereda typer och asynkrona processer, vara medveten om versionshantering och transaktionsdesign samt kapsla in vanliga processer för att förbättra underhållbarheten.
- Att definiera typer i TypeScript och kapsla in IndexedDB-operationer med Promise/async/await förbättrar säkerhet och kodläsbarhet.
- Schemaändringar bör använda versionshantering med
onupgradeneeded, och tung bearbetning bör skjutas upp när det är möjligt. - Utforma transaktioner så att de är korta och undvik tung asynkron bearbetning i samma transaktion.
- Genom att skapa wrapper-klasser kan du minska redundant gemensam kod som felhantering, loggning och typdefinitioner.
Du kan följa med i artikeln ovan med hjälp av Visual Studio Code på vår YouTube-kanal. Vänligen kolla även in YouTube-kanalen.