- 在 React 中使用 IndexedDB 作为本地数据库存储可以有效地管理大量的数据,比如缓存、离线功能或状态持久化。可以通过索引进行快速查询,支持事务处理,并且异步操作。
特点:
-
存储键值对。
-
支持事务。
-
数据可以分层组织为数据库、对象存储(Object Stores)和索引(Indexes)。
-
原生IndexedDB API更灵活,但代码较为复杂。 如果需要在项目中频繁操作 IndexedDB,
idb
是一个轻量但功能强大的选择;如果需要更复杂的查询和事务管理,Dexie
(npm install dexie) 会更适合。
原生IndexedDB的upgradeneeded 事件 & blocked事件
- upgradeneeded 事件的核心作用是帮助你在数据库版本更新时进行结构调整。你可以利用这个事件来:
- 迁移数据:从旧版本的数据库结构迁移到新结构。
- 添加新的存储结构或索引:根据应用的需求,添加新的对象存储或索引,以支持新的功能。
- 清理旧数据:删除过时的对象存储,移除不再需要的数据结构。
- 如果在升级数据库时,另一个实例(如其他浏览器标签页)仍在使用该数据库并且没有关闭,它会阻止当前数据库的打开,导致
blocked
事件被触发。你可以通过监听这个事件来处理这种情况。
示例代码
- 以下代码演示了如何在 IndexedDB 中处理数据库的版本升级过程,包括创建对象存储和索引。它还模拟了打开更高版本的数据库时,如何处理请求被阻塞的情况。
// https://developer.mozilla.org/en-US/docs/Web/API/IDBOpenDBRequest/blocked_event
// 打开数据库,指定数据库名称为 "toDoList",版本号为 4
const DBOpenRequest = window.indexedDB.open("toDoList", 4);
// 当数据库版本需要升级时,触发 onupgradeneeded 事件
DBOpenRequest.onupgradeneeded = (event) => {
const db = event.target.result; // 获取数据库实例
// 如果在创建数据库时发生错误,处理错误并打印错误信息
db.onerror = () => {
console.log("Error creating database");
};
// 在数据库中创建一个名为 "toDoList" 的对象存储(Object Store),以 "taskTitle" 作为主键(keyPath)
const objectStore = db.createObjectStore("toDoList", {
keyPath: "taskTitle", // 任务的标题作为每个数据项的唯一标识符
});
// 在 "toDoList" 对象存储中创建多个索引,分别用于按不同字段排序
objectStore.createIndex("hours", "hours", { unique: false }); // 按小时排序
objectStore.createIndex("minutes", "minutes", { unique: false }); // 按分钟排序
objectStore.createIndex("day", "day", { unique: false }); // 按天排序
objectStore.createIndex("month", "month", { unique: false }); // 按月排序
objectStore.createIndex("year", "year", { unique: false }); // 按年排序
};
// 当数据库打开成功时,触发 onsuccess 事件
DBOpenRequest.onsuccess = (event) => {
// 尝试打开同名数据库,但版本号设为 5,目的是测试数据库版本升级
const req2 = indexedDB.open("toDoList", 5);
// 如果数据库打开请求被阻塞(例如其他窗口正在使用数据库),触发 "blocked" 事件
req2.addEventListener("blocked", () => {
console.log("Request was blocked"); // 打印请求被阻塞的提示
});
};
解释:
-
indexedDB.open("toDoList", 4)
:- 这是用来打开或创建一个名为
"toDoList"
的 IndexedDB 数据库,版本号为 4。 - 如果不存在数据库,会进行创建。
- 如果该数据库版本已经存在且没有更新,它会打开已有的数据库。如果数据库版本号大于当前存在的版本(如从版本 3 升级到版本 4),则会触发
onupgradeneeded
事件(所以版本是个比较重要的参数,测试时要多次修改版本)。
- 这是用来打开或创建一个名为
-
onupgradeneeded
:- 该事件在数据库的版本升级时被触发。这里我们在数据库中创建了一个对象存储(
toDoList
),并定义了几个索引(hours
、minutes
、day
、month
、year
),这些索引将有助于对存储在数据库中的任务进行排序和查询。
- 该事件在数据库的版本升级时被触发。这里我们在数据库中创建了一个对象存储(
-
createObjectStore("toDoList", { keyPath: "taskTitle" })
:- 通过
createObjectStore
创建一个对象存储,keyPath: "taskTitle"
表示对象存储的主键(每个任务的唯一标识符)是taskTitle
字段。
- 通过
-
createIndex()
:- 使用
createIndex
方法为对象存储创建索引,使得在后续查询时可以根据不同的字段(如小时、分钟、日期等)进行排序。
- 使用
使用 idb
库
安装:
npx create-react-app my-app
cd my-app
npm start
npm install idb // 开源地址:https://github.com/jakearchibald/idb
示例代码:
服务组件
import React, { useEffect, useState } from "react";
import { openDB } from "idb";
// 自定义 Hook:用来初始化并返回 IndexedDB 的实例
export const useIndexedDB = () => {
const [db, setDb] = useState(null);
useEffect(() => {
const initDB = async () => {
try {
// 打开或创建数据库
const database = await openDB("MyDatabase", 1, {
upgrade(db) {
// 在升级过程中创建对象存储,类似原生IndexedDB的upgradeneeded 事件
if (!db.objectStoreNames.contains("Users")) {
db.createObjectStore("Users", { keyPath: "id" }); // 设置主键为 id,如果数据没有 id 字段,那么你将无法将它存储在这个对象存储中
// db.createObjectStore(province, { keyPath: 'id', autoIncrement: true }); //这个示例中如果存储的对象没有指定 id 字段,IndexedDB 会自动生成一个唯一的 自增id 字段。
}
},
});
setDb(database); // 将数据库实例存入 state
} catch (error) {
console.error("Failed to open IndexedDB:", error);
}
};
initDB(); // 初始化数据库
}, []); // 只在组件挂载时运行
return db; // 返回数据库实例供调用方使用
};
- objectStoreNames 是一个只读属性,返回数据库中所有的对象存储名称,其提供了一个简单的方式来检查数据库中是否已经存在某个对象存储(objectStore).
组件调用
import React, { useEffect } from "react";
import { useIndexedDB } from "./useIndexedDB";
const App = () => {
const db = useIndexedDB(); // 使用自定义 Hook 获取数据库实例
useEffect(() => {
if (!db) return; // 如果数据库尚未初始化完成,直接返回
const performDBOperations = async () => {
try {
// 添加数据到对象存储
await db.put("Users", { id: 1, name: "Alice", age: 25 });
await db.put("Users", { id: 2, name: "Bob", age: 30 });
// 查询数据
const user = await db.get("Users", 1); // 根据主键 id 查询
console.log("Retrieved user:", user);
// 获取所有数据
const allUsers = await db.getAll("Users");
console.log("All users:", allUsers);
} catch (error) {
console.error("Error performing DB operations:", error);
}
};
performDBOperations();
}, [db]); // 依赖于 db,当 db 初始化完成后执行操作
return <div>IndexedDB Example in React</div>;
};
export default App;
- 结果:
事务
- 事务(
transaction
)是 IndexedDB 中用于封装一组操作的对象。事务保证了一组数据库操作的原子性——要么全部成功,要么全部失败。每次修改数据库时,都需要通过事务来执行。 - 示例:
// 存储每个省份的数据
for (const [province, items] of Object.entries(groupedData)) {
console.log(province,db)
const tx = db.transaction(province, 'readwrite');
const store = tx.objectStore(province);
// 清除现有数据
await store.clear();
// 存储新数据
for (const item of items) {
await store.add(item);
}
await tx.done;
console.log(`${province}的数据已存储`);
}
创建事务 (db.transaction
)
const tx = db.transaction(province, 'readwrite');
db.transaction(province, 'readwrite')
:此行代码用于创建一个数据库事务。db
: 是通过openDB
方法打开的 IndexedDB 数据库实例。province
: 是一个字符串,表示你要操作的对象存储(object store)的名称。在这段代码中,province
应该是某个省份的名称(如 “Beijing”, “Shanghai” 等)。'readwrite'
: 这是事务的模式(transaction mode)。'readwrite'
表示该事务将执行读和写操作。如果你只想读取数据,可以使用'readonly'
模式。
在这里,db.transaction(province, 'readwrite')
创建了一个事务,该事务将针对名为 province
的对象存储执行读写操作。
- 事务模式:
'readonly'
:只能进行读取操作(无法写入数据)。'readwrite'
:可以进行读取和写入操作(包括修改、添加和删除数据)。
获取对象存储(objectStore
)
- 存储对象类似关系型数据库中的表
const store = tx.objectStore(province);
tx.objectStore(province)
:这是事务对象tx
的方法,用于获取指定名称的对象存储(objectStore
)。-
province
: 是你在事务中指定的对象存储名称。在这里,province
是一个省份名称(如 “Beijing”)。 -
tx.objectStore(province)
返回的是该事务中的一个对象存储。对象存储是 IndexedDB 中的数据容器,它可以被认为是数据库中的一个表。每个对象存储包含若干个记录,通常每个记录都有一个主键(key)和数据值(value)。
-
存储对象常用方法
示例:
- 要
Beijing
这个对象存储中添加数据:
const tx = db.transaction('Beijing', 'readwrite');
const store = tx.objectStore('Beijing');
store.add({ id: 1, name: '北京市' }); // 添加一条数据
可以使用以下常用方法来操作数据:
store.add(value, key)
:添加一个新的记录。value
是数据,key
是主键(可选,如果没有提供,IndexedDB 会自动生成一个主键)。store.put(value, key)
:更新或插入一个记录。如果记录已经存在,会更新;如果不存在,会插入新记录。store.get(key)
:根据主键获取一个记录。store.delete(key)
:删除指定主键的记录。store.clear()
:清除对象存储中的所有记录。
事务的提交和完成
tx.done
:表示事务是否完成。当事务完成时,tx.done
会变成resolved
状态。如果事务有任何错误,tx.done
会变成rejected
。
如果正在执行多个操作,可以通过 await tx.done
来等待事务完成:
await tx.done;
console.log('事务已完成');
CG
- 更多相关库: