TypeScript 与 IndexedDB
本文介绍 TypeScript 与 IndexedDB 的相关内容。
我们将通过实际示例来讲解 TypeScript 与 IndexedDB。
YouTube Video
TypeScript 与 IndexedDB
IndexedDB 是一种低级别的 NoSQL 存储,可以在浏览器中持久化结构化数据。通过 TypeScript,可以用类型安全的方式表示数据结构,降低错误率并提升可维护性。
基本术语和工作流程
IndexedDB 是浏览器内部的一个小型数据库。它通过具有名称和版本的数据库、对象存储区、事务、索引和游标等机制来管理数据。数据库具有版本,当版本升级时,会调用 onupgradeneeded 来更新结构,例如创建或修改表。
打开 IndexedDB(基本模式)
首先,展示一个使用 IndexedDB 打开数据库并在 onupgradeneeded 时根据需要创建对象存储的示例。
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));- 此代码打开数据库并记录操作是否成功或失败。
- 如果需要,在
onupgradeneeded中创建todos存储。
在 TypeScript 中定义类型(模型)
接下来,我们用 TypeScript 定义数据类型。这样可以保证后续 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}- 此处定义了
Todo类型。
基本 CRUD 功能实现示例
接下来,演示对对象存储进行增、查、改、删等基本操作。每个函数接收一个 IDBDatabase 并返回一个 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}- 此函数向 IndexedDB 的
todos存储区添加一个新的Todo。返回 Promise 异步处理,操作完成时 resolve。
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}- 此函数检索指定 ID 的
Todo,若找到则返回该对象。若未找到匹配数据,则返回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}- 此函数更新现有的
Todo数据。更新成功时,会记录更新后的Todo的 ID。
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}-
此函数删除指定 ID 的
Todo。删除成功时会记录删除的 ID。 -
这些函数会根据事务的完成或错误来处理(resolve)或拒绝(reject)Promise。通过
console.log输出便于追踪执行过程。
索引和复合查询
利用 IndexedDB 的索引可高效查询特定字段。这里为 createdAt 字段创建索引,并提供范围查询示例。
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}- 此函数打开数据库并对
createdAt字段创建或验证by-createdAt索引。这样可高效按创建时间进行查找和排序。
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}-
此函数仅检索在指定时间戳之后创建的
Todo。使用索引可高效地按创建时间顺序扫描数据。 -
在本示例中,在数据库升级期间创建了
by-createdAt索引,通过游标列举在指定时间之后创建的Todo项。
基于 Promise 的轻量封装器
底层的IndexedDB API编写起来很复杂,重复的类似操作会导致冗余和漏洞。因此,通过准备一个通用的TypeScript包装类来抽象这些操作,我们可以提升代码的简洁性和可维护性。下面给出专注于基础功能的实现。
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 }- 该类封装了 IndexedDB 操作,并提供类型安全的 CRUD 方法。假定对象存储的主键为
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 }- 会在数据库打开时按需新建对象存储。存储初始化通过升级事件完成。
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 }- 向 IndexedDB 存储中添加数据。添加后在控制台输出该数据的 ID。
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 }- 获取指定 ID 的数据。数据不存在则返回
undefined。
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 }- 更新已有数据或添加新数据。处理后记录被更新的数据 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 }- 删除指定 ID 的数据。删除成功后输出被删除的 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}- 获取存储中的所有数据。返回值为类型为
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})();-
下述代码为
IDBWrapper类的实际用法示例。这展示了添加、检索、更新、列出和删除Todo数据的流程。 -
该封装器可简便实现基本的 CRUD 操作。在实际环境中,还需要处理错误处理和结构管理(索引)。
结构迁移(版本升级)
要更改数据库架构,请增加indexedDB.open的第二个参数(版本号),并在onupgradeneeded中进行更新。你需要设计成在现有事务完成后再进行,避免破坏性的更改。
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}onupgradeneeded内的重操作可能导致 UI 阻塞,应尽量精简;如有必要,考虑延迟迁移(如应用启动时分阶段处理)。
事务注意事项(生命周期与错误)
事务会在创建它们的脚本执行结束前自动提交。在事务中使用 await 时,事务可能被提前提交;在同一事务里进行多异步操作需要特别留意。
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}- 注意事务生命周期;必要时使用单独事务,或将操作同步安排在同一个事务内。
游标应用与分页
通过使用游标,你可以顺序处理大规模数据,或者无需使用偏移量就实现简单的分页。
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}- 通过游标逐项读取可减少内存占用。实现分页时通常需记录最后读取的主键。
错误处理与降级方案
1// Feature detection
2if (!('indexedDB' in window)) {
3 console.warn('IndexedDB is not supported. Falling back to localStorage.');
4 // implement fallback logic...
5}- 由于不同浏览器的实现差异或用户的隐私设置(如无痕浏览),IndexedDB 可能无法使用。因此,需要检查
indexedDB是否存在,如果不存在,则提供例如localStorage这样的备选方案。
性能与最佳实践
IndexedDB 快速且强大,但其性能会因设计和数据处理方式不同而有很大差异。根据具体使用场景,可以通过以下方式进行优化:。
- 根据实际使用情况设计对象存储区。例如,如果读操作较多,可以提供索引;如果写操作较多,则应保持键(key)的设计简单。
- 图片、音频等大型二进制数据应以 Blob 形式存储,必要时可通过 File API 或 service worker 进行管理。必要时也可以考虑进行压缩。
- 尽量缩短事务时间,繁重的处理应在事务外进行,以减少锁定时间。
- 索引能加快查询速度,但会降低插入和更新速度,因此只创建真正需要的索引。
- 如果处理大量小数据,使用
getAll()一次性检索可能会耗尽内存。可以通过游标分批处理方式来降低内存使用。
安全性与隐私
IndexedDB 数据根据同源策略按域名和协议隔离。须假设用户删除浏览器数据或使用隐私模式时数据可能会丢失,从而进行设计。
总结与推荐设计模式
要有效地将 TypeScript 与 IndexedDB 结合使用,需准备类型和异步处理,注意版本管理和事务设计,并对常用处理进行封装以提高可维护性。
- 用 TypeScript 定义类型并用 Promise/async/await 封装 IndexedDB 操作能提升安全性与代码可读性。
- 结构变更应通过版本管理和
onupgradeneeded实现,重操作尽可能延后处理。 - 事务应足够短小,避免在同一事务内进行繁重异步处理。
- 通过创建封装类,可以减少错误处理、日志记录和类型定义等重复性的通用处理。
您可以在我们的YouTube频道上使用Visual Studio Code跟随上述文章进行学习。 请也查看我们的YouTube频道。