当需要存储的数据超出 Web Storage 所能承载的量级,或需要更复杂的查询支持时,IndexedDB 便是浏览器端的“重型武器”。它本质上是一个运行在浏览器内的、事务化的、基于键值对的对象数据库。
为什么需要 IndexedDB
在前面两节中,我们讨论了 Cookie 和 Web Storage。它们都有共同的上限:容量小(通常 5–10 MB)、只能存储字符串、没有索引、不支持复杂查询。IndexedDB 则完全不同:
- 大容量:单个源(origin)的存储限额通常可达数百 MB 甚至 GB 级别,具体取决于浏览器和磁盘空间。这为离线缓存大量数据提供了基础。
- 结构化存储:数据库存储的不是字符串,而是 JavaScript 对象(包括数组、嵌套对象、二进制数据等),经过结构化克隆算法保存和读取。
- 异步设计:所有读写操作都是异步的,不会阻塞主线程,因此对大数据的处理不会导致页面卡顿。
- 支持索引与查询:可以创建索引,基于索引进行范围查询、排序,支持游标(cursor)遍历,这在 Web Storage 中完全无法实现。
- 事务机制:所有数据的读写必须在一个事务内完成,保证操作的一致性和原子性。事务有只读、读写版本之分。
核心概念速览
理解 IndexedDB 不需要像理解传统关系型数据库那样漫长,但仍有几个关键概念需要把握。
| 概念 | 简介 |
|------|------|
| 数据库(Database) | 一个域名下可以创建多个数据库,每个数据库有自己独立的名称和版本。 |
| 对象仓库(Object Store) | 类似关系型数据库中的“表”,用来存放一类数据对象。每个仓库有一个主键(key path 或 key generator)。 |
| 索引(Index) | 建立在对象属性上的查询入口,用于快速检索非主键字段。 |
| 事务(Transaction) | 所有操作必须放在一个事务内,事务类型(readonly / readwrite)决定了并发能力。 |
| 请求(Request) | 异步操作的返回载体,通过 onsuccess / onerror 事件获取结果或捕获错误。 |
基本操作:从打开到增删改查
IndexedDB 的原生 API 采用回调(事件)风格,代码量相对较多。下面展示一个典型的读写流程,并同时给出基于 Promise 的现代封装(原生 API 现在已经支持部分 Promise,但并非所有浏览器完全覆盖,因此常用类库或自定义封装)。
打开数据库并创建对象仓库
// 打开(或创建)数据库,版本号决定是否触发升级事件
const request = indexedDB.open('MyAppDB', 1);
// 数据库版本更新时触发(首次创建或升级版本号大于现有版本)
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 创建一个对象仓库,主键为 id 属性,自动递增
const store = db.createObjectStore('users', {
keyPath: 'id',
autoIncrement: true
});
// 为 name 字段创建索引,允许非唯一值
store.createIndex('nameIndex', 'name', { unique: false });
};
request.onsuccess = () => {
console.log('数据库打开成功');
const db = request.result;
// 后续操作在此进行
};
request.onerror = () => {
console.error('数据库打开失败', request.error);
};
添加数据(需在事务中)
function addUser(db, user) {
// 创建读写事务,指定操作的对象仓库
const transaction = db.transaction('users', 'readwrite');
const store = transaction.objectStore('users');
const addRequest = store.add(user);
addRequest.onsuccess = () => {
console.log('用户添加成功,id:', addRequest.result);
};
addRequest.onerror = () => {
console.error('添加失败', addRequest.error);
};
}
读取数据
function getUser(db, id) {
const transaction = db.transaction('users', 'readonly');
const store = transaction.objectStore('users');
const getRequest = store.get(id);
getRequest.onsuccess = () => {
console.log('查询结果:', getRequest.result);
};
}
使用索引查询
function getUserByName(db, name) {
const transaction = db.transaction('users', 'readonly');
const store = transaction.objectStore('users');
const index = store.index('nameIndex');
const query = index.get(name); // 返回第一个匹配项
query.onsuccess = () => {
console.log('通过 name 索引找到:', query.result);
};
// 若要获取多个结果,可使用游标
const keyRange = IDBKeyRange.only(name);
index.openCursor(keyRange).onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
console.log(cursor.value);
cursor.continue();
}
};
}
更新与删除
更新使用 put(会覆盖同主键记录),删除使用 delete,方法同在事务内的对象仓库上调用,完全类似 add 和 get。
异步痛点与 Promise 化封装
原生 IndexedDB 基于事件回调,容易形成“回调地狱”。在实际项目中,我们通常使用成熟的封装库,如 idb(Google 推出的轻量级 Promise 封装)或 localForage(将 IndexedDB 降级为 Web Storage 的统一接口)。也可以用简单的辅助函数手动 Promise 化:
function promisify(request) {
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 使用
const db = await promisify(indexedDB.open('test', 1));
const tx = db.transaction('store', 'readwrite');
const store = tx.objectStore('store');
await promisify(store.add({ name: 'Alice' }));
真实场景应用
IndexedDB 不适合用来替代后端数据库,但在前端领域,它在以下场景中价值很高:
- 离线应用(PWA):缓存大量页面资源、用户生成内容、草稿等。结合 Service Worker,可以实现完全离线的 Web 应用。
- 大量结构化数据的本地处理:例如管理数千条联系人、邮件、待办事项的数据,并提供排序、搜索功能。
- 数据分析工具:将接口返回的大量 JSON 数据存储到 IndexedDB,利用索引和游标进行前端侧的过滤分析,避免频繁请求服务器。
- 游戏素材缓存:Web 游戏中角色、地图等资源大可不必每次加载,直接存储到 IndexedDB 中,减少重复下载。
使用注意事项
- 容量并非无限:各浏览器策略不同,超出额度后会触发配额管理弹窗或拒绝写入,应做好错误处理。
- 事务生命周期:事务在浏览器事件循环的一次微任务完成后自动提交,所有操作必须在事务创建后尽快执行,不能跨异步间隔,否则会触发
TransactionInactiveError。 - 版本管理:添加或删除对象仓库、修改索引必须通过提升数据库版本号来实现,这需要谨慎操作,尤其是在线上多用户同时打开页面时。
- 隐私模式下行为有差异:部分浏览器在隐私模式下不支持 IndexedDB 或大幅限制存储容量。
IndexedDB 的 API 虽然原始,但它为现代 Web 应用提供了接近原生应用的离线能力和数据管理能力。掌握它的基础操作,并善用社区封装,就能在需要存储大量结构化前端数据时游刃有余。