一、 引言:为什么是 IndexedDB?
在现代 Web 开发中,我们常常需要:
- 让应用在离线状态下依然可用
- 在浏览器端存储大量数据(如完整的产品目录、用户日志)
- 对本地数据进行高效的复杂查询(如排序、筛选、分页)
传统的 localStorage
和 cookies
因其容量小(~5MB)、API 同步、无法存储复杂类型等缺点,完全无法胜任这些任务。
IndexedDB 就是为此而生的浏览器内置的非关系型数据库。它具有:
- 巨大的存储空间:通常可达硬盘的 50% 以上。
- 异步操作:不会阻塞 UI 线程,性能卓越。
- 支持事务:保证数据操作的原子性和一致性。
- 丰富的查询能力:通过索引支持高效查询。
- 支持二进制数据 :可以存储
ArrayBuffer
、Blob
等类型。
二、 核心概念:理解 IndexedDB 的"世界观"
开始编码前,务必先理解这几个核心概念:
-
数据库 (Database):
- 每个源(协议+域名+端口)一个数据库,同源策略是最高隔离规则。
- 通过版本号 (
version
) 来管理数据库结构变更。
-
对象仓库 (Object Store):
- 核心概念,相当于 SQL 中的表,是存储数据的主要容器。
- 创建时可定义主键 (
keyPath
) 和自增选项 (autoIncrement
)。
-
索引 (Index):
- 在对象仓库上创建的结构,用于快速查询非主键字段。
- 可以强制唯一性 (
unique: true
)。
-
事务 (Transaction):
- 所有操作必须在事务内进行。事务提供原子性:要么全部成功,要么全部失败。
- 有三种模式:
readonly
(只读)、readwrite
(读写)、versionchange
(结构变更)。
-
游标 (Cursor):
- 一种迭代机制,用于遍历对象仓库或索引中的大量记录。
-
请求 (Request) & 事件 (Event):
- IndexedDB API 是异步的、基于事件的 。每个操作都会返回一个
IDBRequest
对象,通过监听其onsuccess
和onerror
事件来处理结果。
- IndexedDB API 是异步的、基于事件的 。每个操作都会返回一个
三、 实战:从创建到CRUD
让我们通过一个"用户管理系统"的示例来学习完整流程。我们将创建一个 users
表,包含 id
(主键)、name
、email
(唯一索引)和 age
字段。
第 1 步:打开/创建数据库
任何操作的第一步都是打开数据库。如果数据库不存在,则会创建它。
关键 :数据库结构的变更(创建/删除对象仓库和索引)必须在 onupgradeneeded
事件中进行。
javascript
// 配置数据库名称和版本
const dbName = 'UserDB';
const dbVersion = 1; // 从 1 开始。要修改结构,必须增加此版本号
const request = indexedDB.open(dbName, dbVersion);
let db; // 用于保存数据库实例的全局变量
request.onerror = (event) => {
console.error('❌ 数据库打开失败:', event.target.error);
};
request.onsuccess = (event) => {
console.log('✅ 数据库打开成功!');
db = event.target.result; // 将数据库实例保存到全局变量中
// 现在可以执行其他操作了,例如 getAllUsers()
};
// 这个事件在数据库版本升级(或初次创建)时触发
request.onupgradeneeded = (event) => {
console.log('🔄 数据库升级/初始化中...');
const db = event.target.result; // 这里也可以获取到数据库实例
// 检查对象仓库是否存在,如果不存在则创建
if (!db.objectStoreNames.contains('users')) {
// 创建对象仓库 (表),指定主键为 'id',并自增
const objectStore = db.createObjectStore('users', {
keyPath: 'id',
autoIncrement: true
});
// 创建索引
// 参数:索引名称、键路径、配置选项(如是否唯一)
objectStore.createIndex('name', 'name', { unique: false });
objectStore.createIndex('email', 'email', { unique: true }); // 邮箱必须唯一
objectStore.createIndex('age', 'age', { unique: false });
console.log('✅ 对象仓库和索引创建完毕');
}
};
### 第 2 步:添加数据 (Create)
使用 `add()` 方法(如果主键存在则报错)或 `put()` 方法(如果主键存在则更新)。
```javascript
function addUser(userData) {
// 1. 开启一个读写事务,指定涉及的对象仓库名称
const transaction = db.transaction(['users'], 'readwrite');
// 2. 获取对象仓库
const objectStore = transaction.objectStore('users');
// 3. 执行添加操作
const request = objectStore.add(userData);
request.onsuccess = () => {
console.log('✅ 用户数据已添加,ID:', request.result); // 如果是自增主键,result 是新ID
};
request.onerror = (event) => {
console.error('❌ 添加失败:', event.target.error);
};
// 事务完成事件(所有操作都成功)
transaction.oncomplete = () => {
console.log('🟢 添加事务已完成');
};
}
// 调用示例
addUser({ name: '张三', email: 'zhangsan@example.com', age: 30 });
第 3 步:读取数据 (Read)
使用 get()
方法通过主键获取数据,或通过索引查询。
javascript
// 通过主键读取
function getUser(userId) {
const transaction = db.transaction(['users']); // 默认为 'readonly' 事务
const objectStore = transaction.objectStore('users');
const request = objectStore.get(userId); // get() 方法接收主键值
request.onsuccess = () => {
const user = request.result;
if (user) {
console.log('✅ 找到用户:', user);
} else {
console.log('⚠️ 未找到该用户');
}
};
request.onerror = (event) => {
console.error('❌ 读取失败:', event.target.error);
};
}
// 通过索引读取 (例如,通过邮箱)
function getUserByEmail(email) {
const transaction = db.transaction(['users']);
const objectStore = transaction.objectStore('users');
const emailIndex = objectStore.index('email'); // 获取索引
const request = emailIndex.get(email); // 在索引上使用 get
request.onsuccess = () => {
console.log('✅ 通过邮箱找到用户:', request.result);
};
}
// 调用示例
getUser(1);
getUserByEmail('zhangsan@example.com');
第 4 步:更新数据 (Update)
使用 put()
方法。它会用提供的新对象完全替换主键对应的原有对象。
javascript
function updateUser(user) {
// 注意:user 对象必须包含 id (主键) 字段,否则会变成新增
const transaction = db.transaction(['users'], 'readwrite');
const objectStore = transaction.objectStore('users');
const request = objectStore.put(user); // 使用 put 进行更新
request.onsuccess = () => {
console.log('✅ 用户数据已更新');
};
}
// 调用示例:假设我们要更新 id 为 1 的用户年龄
// 先获取,再修改,最后更新
getUser(1);
// 在 get 的 onsuccess 中:
// const user = request.result;
// user.age = 31;
// updateUser(user);
第 5 步:删除数据 (Delete)
使用 delete()
方法通过主键删除数据。
javascript
function deleteUser(userId) {
const transaction = db.transaction(['users'], 'readwrite');
const objectStore = transaction.objectStore('users');
const request = objectStore.delete(userId);
request.onsuccess = () => {
console.log('✅ 用户已删除');
};
}
第 6 步:遍历所有数据 (使用游标)
当需要获取所有数据或批量操作时,游标是唯一的选择。
javascript
function getAllUsers() {
const transaction = db.transaction(['users']);
const objectStore = transaction.objectStore('users');
const request = objectStore.openCursor(); // 打开游标
const allUsers = [];
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
// cursor.value 是当前的数据对象
allUsers.push(cursor.value);
cursor.continue(); // 移动游标到下一条记录
} else {
// 游标遍历完毕
console.log('📋 所有用户:', allUsers);
}
};
}
四、 在 Chrome DevTools 中调试
作为前端开发,熟练使用开发者工具至关重要。 Chrome DevTools 了解详情
- 打开 Chrome DevTools (F12)。
- 切换到 Application (应用程序) 面板。
- 在左侧展开 IndexedDB -> YourDatabaseName -> YourObjectStore。
- 在这里你可以:
- 查看所有存储的数据:以表格形式展示,非常清晰。
- 直接编辑数据:双击即可修改字段值。
- 删除数据:右键点击记录即可删除。
- 清空整个对象仓库 :点击右侧的 Clear object store 按钮。
- 刷新数据 :当数据变更时,点击右侧的 Refresh 按钮。
- 删除整个数据库 :右键数据库名选择 Delete database。

(示意图:你可以在 DevTools 中直观地管理和调试你的 IndexedDB 数据)
五、 最佳实践与高级技巧
-
使用 Promise 包装 :原生回调风格非常繁琐,建议封装成 Promise 或使用
async/await
。javascript// 一个简单的 Promise 封装示例 function promisifyRequest(request) { return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async function addUserAsync(userData) { const transaction = db.transaction(['users'], 'readwrite'); const objectStore = transaction.objectStore('users'); try { const id = await promisifyRequest(objectStore.add(userData)); console.log('用户添加成功,ID:', id); await promisifyRequest(transaction); // 等待事务完成 } catch (error) { console.error('操作失败:', error); } }
更推荐 :直接使用成熟的库(如 idb),它提供了非常优雅的 Promise 化 API。
-
处理版本迁移 :在
onupgradeneeded
中,可以通过event.oldVersion
来编写复杂的升级逻辑。javascriptrequest.onupgradeneeded = (event) => { const db = event.target.result; const oldVersion = event.oldVersion; // 旧版本号,如果是从0创建,则为0 if (oldVersion < 1) { // 初始版本创建逻辑 const store = db.createObjectStore('users', { keyPath: 'id', autoIncrement: true }); store.createIndex('name', 'name'); } if (oldVersion < 2) { // 版本2升级逻辑:为users表添加age字段的索引 const transaction = event.target.transaction; const store = transaction.objectStore('users'); store.createIndex('age', 'age'); } if (oldVersion < 3) { // 版本3升级逻辑:创建另一个对象仓库 db.createObjectStore('logs', { keyPath: 'timestamp' }); } };
-
错误处理 :务必为所有请求和事务添加
onerror
事件监听,否则错误会被静默吞掉,极难调试。 -
性能优化:
- 对于大量数据的循环操作,使用游标比
getAll()
更节省内存。 - 将多个操作放在同一个事务中,效率远高于开启多个独立事务。
- 对于大量数据的循环操作,使用游标比
六、 总结
IndexedDB 是构建现代、强大、离线化 Web 应用的基石。它的学习曲线虽然比 localStorage
陡峭,但其带来的能力提升是质的飞跃。
核心流程回顾:
- 打开数据库 (
indexedDB.open
) -> 处理onupgradeneeded
。 - 创建事务 (
db.transaction
) -> 指定模式和对象仓库。 - 获取对象仓库 (
transaction.objectStore
) -> 执行操作的入口。 - 执行操作 (
get
,add
,put
,delete
,openCursor
) -> 进行CRUD。 - 处理结果 -> 监听请求的
onsuccess
/onerror
事件。
希望这篇教程能帮助你彻底征服 IndexedDB,为你下一个出色的离线 Web 应用打下坚实的基础!
进一步学习: