TypeScript dan IndexedDB
Artikel ini menjelaskan tentang TypeScript dan IndexedDB.
Kami akan menjelaskan TypeScript dan IndexedDB dengan contoh praktis.
YouTube Video
TypeScript dan IndexedDB
IndexedDB adalah penyimpanan NoSQL tingkat rendah yang memungkinkan Anda menyimpan data terstruktur di browser. Dengan TypeScript, Anda dapat merepresentasikan skema secara type-safe, mengurangi kesalahan dan meningkatkan pemeliharaan.
Terminologi Dasar dan Alur Kerja
IndexedDB adalah sebuah database kecil di dalam browser. Ini mengelola data menggunakan mekanisme seperti database dengan nama dan versi, object store, transaksi, indeks, dan kursor. Database memiliki versi, dan ketika versi diperbarui, onupgradeneeded akan dipanggil untuk memperbarui skema, seperti membuat atau memodifikasi tabel.
Membuka IndexedDB (pola dasar)
Pertama, kami menunjukkan contoh membuka database dengan IndexedDB dan membuat object store di onupgradeneeded jika diperlukan.
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));- Kode ini membuka database dan mencatat apakah berhasil atau gagal.
- Jika diperlukan, store
todosdibuat dionupgradeneeded.
Mendefinisikan tipe di TypeScript (Model)
Selanjutnya, kita mendefinisikan tipe data menggunakan TypeScript. Ini memastikan keamanan tipe pada operasi CRUD berikutnya.
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}- Di sini, kami mendefinisikan tipe
Todo.
Contoh implementasi fungsi CRUD sederhana
Selanjutnya, kami menunjukkan operasi CRUD dasar seperti menambah, mengambil, memperbarui, dan menghapus dari object store. Setiap fungsi menerima IDBDatabase dan mengembalikan 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}- Fungsi ini menambahkan
Todobaru ke storetodosdi IndexedDB. Ini mengembalikan Promise untuk penanganan asinkron, yang selesai saat proses selesai.
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}- Fungsi ini mengambil
Tododengan ID yang ditentukan dan mengembalikan objek jika ditemukan. Jika data yang sesuai tidak ditemukan, akan mengembalikanundefined.
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}- Fungsi ini memperbarui data
Todoyang sudah ada. Jika berhasil, ID dariTodoyang diperbarui dicatat.
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}-
Fungsi ini menghapus
Tododengan ID yang ditentukan. Jika proses berhasil, ID yang dihapus dicatat. -
Fungsi-fungsi ini menyelesaikan atau menolak Promise tergantung pada penyelesaian transaksi atau terjadinya error. Menambahkan output melalui
console.logmemudahkan untuk melacak apa yang terjadi selama eksekusi.
Indeks dan query gabungan
Dengan menggunakan indeks di IndexedDB, Anda dapat mencari bidang tertentu secara efisien. Di sini, kita membuat indeks untuk createdAt dan memberikan contoh query rentang.
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}- Fungsi ini membuka database dan membuat atau memverifikasi indeks
by-createdAtpada fieldcreatedAt. Ini memungkinkan pencarian dan pengurutan berdasarkan tanggal pembuatan secara efisien.
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}-
Fungsi ini hanya mengambil
Todosyang dibuat setelah waktu yang ditentukan. Menggunakan indeks memungkinkan pemindaian data secara efisien berdasarkan urutan tanggal pembuatan. -
Pada contoh ini, indeks
by-createdAtdibuat selama upgrade database, dan itemTodoyang dibuat setelah waktu tertentu diurutkan dengan kursor.
Wrapper ringan berbasis Promise
API IndexedDB tingkat rendah sulit untuk ditulis, dan operasi serupa yang berulang dapat menyebabkan redundansi dan bug. Oleh karena itu, dengan menyiapkan kelas pembungkus TypeScript generik yang mengabstraksi operasi-operasi tersebut, kita meningkatkan kesederhanaan dan kemampuan pemeliharaan kode. Di bawah ini adalah implementasi yang berfokus pada fungsi dasar.
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 }- Kelas ini membungkus operasi IndexedDB dan menyediakan metode CRUD yang type-safe. Ini mengasumsikan object store dengan kunci adalah
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 }- Ini membuka database dan membuat object store baru jika diperlukan. Inisialisasi store dilakukan menggunakan event upgrade.
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 }- Menambahkan data ke store IndexedDB. Setelah menambah, ID dicatat ke konsol.
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 }- Mengambil data sesuai dengan ID yang ditentukan.
undefinedakan dikembalikan jika data tidak ada.
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 }- Memperbarui data yang ada atau menambah data baru. Setelah diproses, ID yang diperbarui dicatat.
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 }- Menghapus data dengan ID yang ditentukan. Jika berhasil, ID yang dihapus dicatat.
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}- Mengambil semua data di store. Nilai kembalian adalah array bertipe
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})();-
Kode ini adalah contoh penggunaan nyata dari kelas
IDBWrapper. Ini menunjukkan alur untuk menambah, mengambil, memperbarui, menampilkan daftar, dan menghapus dataTodo. -
Wrapper ini memungkinkan penanganan operasi CRUD dasar secara sederhana. Dalam lingkungan nyata, Anda juga perlu menangani penanganan error dan manajemen skema (indeks).
Migrasi skema (peningkatan versi)
Untuk mengubah skema basis data, tingkatkan argumen kedua (versi) dari indexedDB.open dan perbarui dalam onupgradeneeded. Anda perlu merancangnya agar transaksi yang ada telah selesai dan perubahan destruktif dapat dihindari.
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}- Pemrosesan berat di dalam
onupgradeneededdapat memblokir UI, jadi minimalisir, dan pertimbangkan migrasi bertahap (pemrosesan staging saat startup aplikasi) jika memungkinkan.
Perhatian tentang transaksi (siklus hidup dan kesalahan)
Transaksi secara otomatis dikomit sebelum eksekusi skrip yang membuatnya selesai. Saat menggunakan await di dalam transaksi, transaksi dapat dikomit dengan tak terduga; berhati-hatilah saat melakukan beberapa operasi async dalam satu transaksi.
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}- Perhatikan masa aktif transaksi; gunakan transaksi terpisah jika perlu, atau jadwalkan operasi secara sinkron dalam transaksi.
Aplikasi kursor dan paginasi
Dengan menggunakan kursor, Anda dapat memproses data skala besar secara berurutan atau menerapkan paginasi sederhana tanpa menggunakan 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}- Dengan mengambil item satu per satu menggunakan kursor, penggunaan memori dapat dikurangi. Saat mengimplementasikan paging, umum untuk mengingat kunci yang terakhir dibaca.
Penanganan error dan 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 mungkin tidak tersedia karena perbedaan implementasi antara peramban atau pengaturan privasi pengguna, seperti mode penjelajahan pribadi. Oleh karena itu, periksa apakah
indexedDBtersedia dan, jika tidak, sediakan alternatif sepertilocalStorage.
Performa dan praktik terbaik
IndexedDB cepat dan kuat, tetapi performanya bisa sangat bervariasi tergantung pada desain dan bagaimana data dikelola. Tergantung pada kasus penggunaan, optimisasi dapat dilakukan dengan cara berikut:.
- Desain object store sesuai dengan penggunaan yang sebenarnya. Misalnya, jika banyak pembacaan, sediakan indeks; jika banyak penulisan, buat desain key yang sederhana.
- Data biner besar seperti gambar dan audio sebaiknya disimpan sebagai Blob, atau dikelola menggunakan File API atau service worker jika diperlukan. Kompresi juga dapat dipertimbangkan jika diperlukan.
- Buat transaksi sesingkat mungkin dan lakukan pemrosesan berat di luar transaksi untuk meminimalkan waktu penguncian.
- Indeks dapat mempercepat pencarian, tetapi memperlambat penambahan dan perubahan data, jadi buatlah hanya yang benar-benar diperlukan.
- Saat menangani banyak data kecil, mengambil semuanya sekaligus dengan
getAll()dapat menghabiskan memori. Anda dapat mengurangi penggunaan memori dengan membagi pemrosesan menggunakan kursor.
Keamanan dan privasi
Data IndexedDB terisolasi per domain dan protokol sesuai kebijakan same-origin. Desainlah dengan asumsi bahwa data dapat hilang jika pengguna menghapus data browser atau menggunakan mode privat.
Ringkasan dan pola desain yang direkomendasikan
Untuk menggunakan IndexedDB secara efektif dengan TypeScript, penting untuk menyiapkan tipe dan proses asinkron, memperhatikan manajemen versi dan desain transaksi, serta membungkus proses umum untuk meningkatkan pemeliharaan.
- Mendefinisikan tipe di TypeScript dan membungkus operasi IndexedDB dengan Promise/async/await meningkatkan keamanan dan keterbacaan kode.
- Perubahan skema harus menggunakan manajemen versi dengan
onupgradeneeded, dan pemrosesan berat sebaiknya ditunda jika memungkinkan. - Desain transaksi agar tetap singkat dan hindari pemrosesan asinkron yang berat dalam satu transaksi yang sama.
- Dengan membuat kelas pembungkus (wrapper), Anda dapat mengurangi proses umum yang berulang seperti penanganan error, pencatatan log, dan definisi tipe.
Anda dapat mengikuti artikel di atas menggunakan Visual Studio Code di saluran YouTube kami. Silakan periksa juga saluran YouTube kami.