TypeScript dan IndexedDB

TypeScript dan IndexedDB

Artikel ini menerangkan tentang TypeScript dan IndexedDB.

Kami akan menerangkan TypeScript dan IndexedDB dengan contoh praktikal.

YouTube Video

TypeScript dan IndexedDB

IndexedDB ialah storan NoSQL tahap rendah yang membolehkan anda menyimpan data berstruktur dalam pelayar. Dengan TypeScript, anda boleh mewakili skema secara type-safe, mengurangkan ralat dan meningkatkan kebolehselenggaraan.

Istilah Asas dan Aliran Kerja

IndexedDB ialah pangkalan data kecil di dalam pelayar. Ia mengurus data menggunakan mekanisme seperti pangkalan data dengan nama dan versi, stor objek, transaksi, indeks, dan kursor. Pangkalan data mempunyai versi, dan apabila versi dinaik taraf, onupgradeneeded akan dipanggil untuk mengemas kini skema, seperti mencipta atau mengubah jadual.

Membuka IndexedDB (pola asas)

Pertama, kami tunjukkan contoh membuka pangkalan data dengan IndexedDB dan mencipta object store dalam onupgradeneeded jika perlu.

 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));
  • Kod ini membuka pangkalan data dan mencatat sama ada ia berjaya atau gagal.
  • Jika perlu, stor todos akan dicipta dalam onupgradeneeded.

Mendefinisikan jenis dalam TypeScript (Model)

Seterusnya, kami akan mentakrifkan jenis data menggunakan TypeScript. Ini memastikan keselamatan jenis dalam operasi CRUD seterusnya.

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 takrifkan jenis Todo.

Contoh pelaksanaan fungsi CRUD ringkas

Seterusnya, kami tunjukkan operasi CRUD asas seperti menambah, mendapatkan semula, mengemas kini, dan memadam 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 menambah Todo baharu ke dalam stor todos di IndexedDB. Ia mengembalikan Promise untuk pengendalian tak serentak, yang diselesaikan apabila pemprosesan 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 mendapatkan Todo dengan ID yang ditentukan dan memulangkan objek jika dijumpai. Jika tiada data padanan dijumpai, ia mengembalikan undefined.
 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 mengemas kini data Todo sedia ada. Jika berjaya, ID Todo yang dikemas kini dicatatkan.
 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 memadam Todo dengan ID yang ditentukan. Apabila pemprosesan berjaya, ID yang dipadam akan dicatatkan.

  • Fungsi-fungsi ini akan melengkapkan atau menolak Promise bergantung pada kejayaan atau ralat transaksi. Menambah output melalui console.log memudahkan penjejakan sepanjang pelaksanaan.

Indeks dan pertanyaan kompaun

Dengan menggunakan indeks dalam IndexedDB, anda boleh mencari secara cekap pada medan tertentu. Di sini, kami cipta indeks untuk createdAt dan berikan contoh pertanyaan jangkauan.

 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 pangkalan data dan cipta atau sahkan indeks by-createdAt pada medan createdAt. Ini membolehkan pencarian dan pengisihan secara cekap mengikut tarikh penciptaan.
 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 mendapatkan Todo yang dicipta selepas cap masa yang ditentukan. Menggunakan indeks membolehkan pengimbasan data secara cekap mengikut tarikh penciptaan.

  • Dalam contoh ini, indeks by-createdAt dibuat semasa naik taraf pangkalan data, dan item Todo yang diciptakan selepas masa yang ditentukan disenaraikan dengan kursor.

Pembungkus ringan berasaskan Promise

API IndexedDB peringkat rendah adalah rumit untuk ditulis, dan operasi yang serupa yang berulang boleh menyebabkan redundansi dan pepijat. Oleh itu, dengan menyediakan kelas pembungkus TypeScript generik yang mengabstrakkan operasi, kita meningkatkan kesederhanaan dan kebolehselenggaraan kod. Di bawah ialah pelaksanaan yang fokus pada fungsi asas.

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 kaedah CRUD yang type-safe. Ia menganggap object store tersebut menggunakan id sebagai kunci.
 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  }
  • Ia membuka pangkalan data dan cipta object store baharu jika diperlukan. Inisialisasi stor dilakukan semasa peristiwa naik taraf.
 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  }
  • Menambah data ke stor IndexedDB. Selepas ditambah, ID akan 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  }
  • Mendapatkan data mengikut ID yang tertentu. undefined akan dikembalikan jika data tiada.
 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  }
  • Mengemaskini data sedia ada atau menambah data baharu. Selepas diproses, ID yang dikemas kini akan 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  }
  • Memadam data dengan ID tertentu. Jika berjaya, ID yang dipadam akan 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}
  • Mendapatkan semua data dalam stor. Nilai yang dikembalikan ialah array jenis 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})();
  • Kod ini ialah contoh penggunaan sebenar kelas IDBWrapper. Ini menunjukkan aliran untuk menambah, mendapatkan, mengemas kini, menyenaraikan, dan memadam data Todo.

  • Pembungkus ini membolehkan pengendalian mudah operasi CRUD asas. Dalam persekitaran sebenar, anda juga perlu mengendalikan pengurusan ralat dan pengurusan skema (indeks).

Migrasi skema (naik taraf versi)

Untuk menukar skema pangkalan data, tingkatkan argumen kedua (versi) bagi indexedDB.open dan kemaskini di dalam onupgradeneeded. Anda perlu mereka bentuk supaya transaksi sedia ada telah selesai dan perubahan merosakkan dapat dielakkan.

 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}
  • Pemprosesan berat dalam onupgradeneeded boleh menyebabkan UI tersekat, jadi pastikan ia seminimum mungkin dan pertimbangkan migrasi tertunda (pemprosesan peringkat semasa permulaan aplikasi) jika boleh.

Perhatian tentang transaksi (kitar hayat dan ralat)

Transaksi akan dikomit secara automatik sebelum skrip yang menciptanya tamat dijalankan. Apabila menggunakan await dalam transaksi, ia mungkin dikomit secara tak disangka; berhati-hati jika melakukan pelbagai operasi async dalam transaksi yang sama.

 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}
  • Ambil perhatian pada jangka hayat transaksi; gunakan transaksi berasingan jika perlu, atau jadualkan operasi secara segerak dalam sebuah transaksi.

Aplikasi kursor dan pembahagian halaman

Dengan menggunakan kursor, anda boleh memproses data berskala besar secara berurutan atau melaksanakan penomboran halaman ringkas 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 demi satu menggunakan kursor, penggunaan memori boleh dikurangkan. Semasa melaksanakan paging, biasanya mengingat kunci bacaan terakhir.

Pengendalian ralat dan kaedah sandaran

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 disebabkan perbezaan implementasi antara pelayar atau tetapan privasi pengguna, seperti pelayaran peribadi. Oleh itu, periksa sama ada indexedDB wujud dan, jika tidak, sediakan alternatif seperti localStorage.

Prestasi dan amalan terbaik

IndexedDB adalah pantas dan berkuasa, tetapi prestasi boleh berbeza dengan ketara bergantung pada reka bentuk dan cara data dikendalikan. Bergantung pada kes penggunaan, pengoptimuman boleh dilakukan dengan cara berikut:.

  • Reka stor objek mengikut penggunaan sebenar. Contohnya, jika terdapat banyak pembacaan, sediakan indeks; jika terdapat banyak penulisan, pastikan reka bentuk kekunci ringkas.
  • Data binari yang besar seperti imej dan audio harus disimpan sebagai Blob, atau diuruskan menggunakan File API atau service worker jika perlu. Pemampatan juga boleh dipertimbangkan jika perlu.
  • Pastikan transaksi sependek mungkin dan lakukan pemprosesan berat di luar transaksi untuk meminimumkan masa kunci.
  • Indeks boleh mempercepatkan pencarian, tetapi memperlahankan masukan dan kemas kini, jadi cipta hanya yang benar-benar diperlukan.
  • Apabila berurusan dengan banyak data kecil, mengambil semuanya sekaligus menggunakan getAll() mungkin akan menghabiskan memori. Anda boleh mengurangkan penggunaan memori dengan membahagikan pemprosesan menggunakan kursor.

Keselamatan dan privasi

Data IndexedDB diasingkan mengikut domain dan protokol berdasarkan polisi asal yang sama. Reka bentuk dengan anggapan bahawa data mungkin hilang jika pengguna memadam data pelayar atau menggunakan mod peribadi.

Ringkasan dan corak reka bentuk disyorkan

Untuk menggunakan IndexedDB dengan berkesan bersama TypeScript, adalah penting untuk menyediakan jenis dan proses tak segerak, sedar tentang pengurusan versi dan reka bentuk transaksi, serta membungkus pemprosesan biasa untuk meningkatkan kebolehselenggaraan.

  • Mendefinisikan jenis dalam TypeScript dan membungkus operasi IndexedDB dengan Promise/async/await meningkatkan keselamatan dan kebolehbacaan kod.
  • Perubahan skema harus menggunakan pengurusan versi dengan onupgradeneeded, dan pemprosesan berat sebaiknya dilewatkan jika boleh.
  • Reka bentuk transaksi agar ringkas dan elakkan pemprosesan async berat dalam transaksi yang sama.
  • Dengan mencipta kelas pembungkus, anda boleh mengurangkan proses biasa yang berulang seperti pengendalian ralat, pencatatan, dan takrif jenis.

Anda boleh mengikuti artikel di atas menggunakan Visual Studio Code di saluran YouTube kami. Sila lihat juga saluran YouTube kami.

YouTube Video