TypeScript at IndexedDB
Ipinapaliwanag ng artikulong ito ang tungkol sa TypeScript at IndexedDB.
Ipapaliwanag namin ang TypeScript at IndexedDB gamit ang mga praktikal na halimbawa.
YouTube Video
TypeScript at IndexedDB
Ang IndexedDB ay isang low-level na NoSQL storage na nagpapahintulot sa iyo na magsalba ng structured data sa browser. Sa TypeScript, maaari mong irepresenta ang mga schema sa ligtas na paraan, na nagpapababa ng mga error at nagpapahusay sa maintainability.
Pangunahing Terminolohiya at Daloy ng Trabaho
Ang IndexedDB ay isang maliit na database sa loob ng browser. Pinamamahalaan nito ang data gamit ang mga mekanismo tulad ng mga database na may pangalan at bersyon, object store, transaksyon, index, at cursor. Ang mga database ay may mga bersyon, at kapag in-upgrade ang bersyon, tinatawag ang onupgradeneeded upang i-update ang schema, tulad ng paglikha o pag-modify ng mga table.
Pagbubukas ng IndexedDB (batayang pattern)
Una, ipapakita namin ang halimbawa ng pagbubukas ng database gamit ang IndexedDB at paggawa ng object store sa onupgradeneeded kung kinakailangan.
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));- Binubuksan ng code na ito ang database at nilalagdaan kung ito ay nagtagumpay o nabigo.
- Kung kinakailangan, ang
todosstore ay nililikha saonupgradeneeded.
Pagdeklara ng mga uri sa TypeScript (Mga Modelo)
Susunod, ideklara natin ang mga uri ng data gamit ang TypeScript. Tinitiyak nito ang kaligtasan ng uri sa susunod na mga operasyon ng CRUD.
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}- Dito, idinedeklara namin ang uri ng
Todo.
Mga halimbawa ng simpleng implementasyon ng CRUD function
Pagkatapos, ipapakita namin ang batayang mga operasyon ng CRUD tulad ng pagdaragdag, pagkuha, pag-update at pag-delete mula sa object store. Bawat function ay tumatanggap ng IDBDatabase at nagbabalik ng isang 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}- Ang function na ito ay nagdadagdag ng bagong
Todosatodosstore ng IndexedDB. Nagbabalik ito ng Promise para sa asynchronous na pagproseso, na magre-resolve kapag natapos na ang proseso.
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}- Kinukuha ng function na ito ang
Todogamit ang tinukoy na ID at ibinabalik ang object kung ito ay natagpuan. Kung walang nahanap na tugmang data, magbabalik ito ngundefined.
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}- Ina-update ng function na ito ang umiiral na data ng
Todo. Kapag matagumpay, nilalagdaan ang ID ng na-update naTodo.
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}-
Binubura ng function na ito ang
Todona may tinukoy na ID. Kapag matagumpay ang proseso, ang naburang ID ay naka-log. -
Inaayos o tinatanggihan ng mga function na ito ang isang Promise depende sa pagtatapos ng transaksyon o error. Ang paggamit ng
console.logay nagpapadali upang subaybayan ang nangyayari habang tumatakbo ang code.
Mga Index at Multiple Queries
Sa paggamit ng mga index sa IndexedDB, maaari kang maghanap nang mas mabilis sa espesipikong alok/acampo. Dito, gumagawa tayo ng index para sa createdAt at nagbibigay ng halimbawa ng range query.
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}- Binubuksan ng function na ito ang database at nililikha o tine-test ang index na
by-createdAtsacreatedAtfield. Nagbibigay ito ng mabilis na paghanap at pag-aayos batay sa petsa ng paglikha.
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}-
Kinukuha lang ng function na ito ang mga
Todona ginawa pagkatapos ng tinukoy na timestamp. Pinahihintulutan ng paggamit ng index ang mabilis na pagkuha sa data batay sa pagkakasunod ng petsa ng pagkakagawa. -
Sa halimbawang ito, ang
by-createdAtindex ay nilikha sa panahon ng pag-upgrade ng database, at ang mgaTodona ginawa matapos ang tinukoy na oras ay nililista gamit ang cursor.
Promise-based na magaan na wrapper
Ang low-level na IndexedDB API ay mahirap isulat, at ang paulit-ulit na magkatulad na operasyon ay maaaring magdulot ng redundancy at mga bug. Dahil dito, sa pamamagitan ng paghahanda ng generic na TypeScript wrapper class na nag-a-abstract ng mga operasyon, napapadali at napapadali ang pag-maintain ng code. Nasa ibaba ang implementasyon na nakatuon sa mga batayang kakayahan.
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 }- Binalot ng class na ito ang mga operasyon ng IndexedDB at nagpo-provide ng type-safe na mga CRUD method. Ipinagpapalagay nito na ang object store ay may key na
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 }- Binubuksan nito ang database at lilikha ng bagong object store kung kinakailangan. Ginagawa ang store initialization gamit ang upgrade event.
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 }- Nagdadagdag ng data sa IndexedDB store. Matapos magdagdag, ang ID ay nilalagay sa console.
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 }- Kinukuha ang data na tumutugma sa tinukoy na ID.
undefinedang ibabalik kung wala ang data.
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 }- Ina-update ang umiiral na data o nagdadagdag ng bagong data. Pagkatapos ng pagproseso, nilalagay sa log ang updated na 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 }- Binubura ang data na may tinukoy na ID. Kapag matagumpay, naka-log ang naburang 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}- Kinukuha ang lahat ng data sa store. Ang ibinabalik ay array ng type na
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})();-
Ang code na ito ay aktwal na halimbawa ng paggamit ng klase na
IDBWrapper. Ipinapakita nito ang proseso ng pagdagdag, pagkuha, pag-update, paglista, at pagbura ngTododata. -
Pinapasimple ng wrapper na ito ang paghawak ng batayang operaciones ng CRUD. Sa aktwal na paggamit, kailangan mo ring asikasuhin ang error handling at schema management (index).
Schema migration (pag-upgrade ng bersyon)
Para baguhin ang database schema, dagdagan ang pangalawang argument (version) ng indexedDB.open at i-update ito sa onupgradeneeded. Kailangan mong idisenyo ito upang ang mga kasalukuyang transaksyon ay matapos muna at maiwasan ang mga destructive na pagbabago.
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}- Ang masyadong mabigat na proseso sa loob ng
onupgradeneededay maaaring magpabagal sa UI, kaya gawing simple lang ito at kung maaari ay gumamit ng delayed migration (i-stage ang proseso sa pagpapatakbo ng app).
Mga pag-iingat sa transaction (lifecycle at mga error)
Ang mga transaction ay awtomatikong naisasara bago matapos ang pagtakbo ng script na lumikha sa kanila. Kapag gumamit ng await sa loob ng transaction, maaaring awtomatikong magsara ito; mag-ingat kapag gumagawa ng maraming async na operasyon sa iisang transaction.
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}- Tandaan ang haba ng buhay ng transaction; gumamit ng magkakahiwalay na transaction kung kinakailangan, o isabay ang operasyon sa iisang transaction.
Cursor applications at pagination
Sa paggamit ng cursor, maaari mong iproseso ang malakihang datos ng sunud-sunod o mag-implement ng simpleng pagination nang hindi gumagamit ng offsets.
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}- Sa pagkuha ng item isa-isa gamit ang cursor, napapababa ang paggamit ng memorya. Kapag nag-iimplement ng paging, karaniwan na tandaan ang huling nabasang key.
Error handling at fallback
1// Feature detection
2if (!('indexedDB' in window)) {
3 console.warn('IndexedDB is not supported. Falling back to localStorage.');
4 // implement fallback logic...
5}- Maaaring hindi available ang IndexedDB dahil sa mga pagkakaiba ng implementasyon sa pagitan ng mga browser o sa privacy settings ng user, tulad ng private browsing. Kaya, tiyaking umiiral ang
indexedDBat kung hindi, magbigay ng alternatibo gaya nglocalStorage.
Performance at pinakamahusay na mga kasanayan
Mabilis at makapangyarihan ang IndexedDB, ngunit maaaring mag-iba ang performance depende sa disenyo at paraan ng paghawak sa data. Depende sa gamit, maaaring gawin ang optimization sa mga sumusunod na paraan:.
- I-disenyo ang object store ayon sa aktwal na gamit. Halimbawa, kung madalas ang pagbabasa, maglagay ng mga index; kung madalas ang pagsusulat, panatilihing simple ang disenyo ng key.
- Ang malalaking binary data tulad ng mga larawan at audio ay dapat itago bilang Blob, o pamahalaan gamit ang File API o service workers kung kinakailangan. Maari ring gumamit ng compression kung kinakailangan.
- Panatilihing maikli ang mga transaksyon at isagawa ang mabibigat na proseso sa labas ng transaksyon upang mabawasan ang lock time.
- Maaaring pabilisin ng mga index ang paghahanap, ngunit nagpapabagal sa insert at update, kaya gumawa lamang ng kinakailangang index.
- Kapag maraming maliliit na data, maaaring maubos ang memorya kapag ginamit ang
getAll()upang kunin lahat ng sabay-sabay. Maaaring bawasan ang paggamit ng memorya sa pamamagitan ng pagproseso gamit ang cursor.
Seguridad at privacy
Ang data ng IndexedDB ay hiwalay kada domain at protocol ayon sa same-origin policy. I-disenyo ito na may pag-aakalang maaaring mawala ang data kapag binura ng user ang browser data o gumamit ng private mode.
Buod at inirerekomendang mga pattern ng disenyo
Para magamit nang epektibo ang IndexedDB sa TypeScript, mahalagang ihanda ang mga type at asynchronous na proseso, maging maingat sa version management at disenyo ng transaksyon, at i-wrap ang mga pangkaraniwang proseso upang mapabuti ang maintainability.
- Ang deklarasyon ng uri sa TypeScript at paggamit ng Promise/async/await sa pag-wrap ng IndexedDB ay nagpapabuti sa kaligtasan at readability ng code.
- Ang pagbabago ng schema ay dapat gumamit ng version management sa
onupgradeneeded, at iwasan ang mabigat na proseso hangga't maaari. - Disenyo ng mga transaction na maging maikli at iwasan ang mabigat na asynchronous na proseso sa parehong transaction.
- Sa pamamagitan ng paggawa ng wrapper classes, maaring mabawasan ang paulit-ulit na proseso tulad ng error handling, pag-log, at pagtatakda ng mga type definition.
Maaari mong sundan ang artikulo sa itaas gamit ang Visual Studio Code sa aming YouTube channel. Paki-check din ang aming YouTube channel.