IndexedDB 是浏览器端真正的数据库,它不是 localStorage 的升级版,而是一个非关系型、异步、支持事务的键值对存储系统。理解什么场景该用它、怎么用它,是前端存储方案选型的关键一环。
适用场景:什么时候该用 IndexedDB
需要存储大量结构化数据
localStorage 只有 5-10MB 的容量(不同浏览器略有差异),超过就会报错。而 IndexedDB 的存储上限通常是浏览器可用磁盘空间的一定比例(可达几百 MB 甚至 GB),且支持复杂的数据结构(对象、数组、二进制等),无需序列化成字符串再存储。如果你的应用需要本地缓存大量数据(如离线小说阅读器、本地笔记应用、PWA 数据持久化),IndexedDB 是不二之选。
数据需要按条件查询,而非仅键值对
localStorage 只能通过 key 精确查找,不能模糊搜索、不能按字段排序、不能分页。IndexedDB 支持创建索引,可以对存储对象的某个字段进行查询、排序、范围过滤,类似于数据库的 WHERE、ORDER BY。例如,在邮件应用中按“已读/未读”筛选,或者在电商缓存中按“价格”排序,只有 IndexedDB 能胜任。
需要事务性和数据一致性
在操作多条相关数据时,如果突然发生错误(比如断电、标签页关闭),IndexedDB 的事务机制能保证要么所有操作全部完成,要么全部回滚,不会出现数据写了一半的不一致状态。localStorage 做不到这一点——你对多个 key 的写入是独立的,中间某一步出问题,前面的修改已经持久化了,无法回滚。
离线优先(Offline-First)或 PWA 应用
Service Worker 本身没有访问 DOM 的权限,但它可以操作 IndexedDB。这意味着可以在后台缓存网络请求结果、本地生成数据,等网络恢复后再同步到服务器。许多 PWA 的数据流都是“优先读写本地 IndexedDB,后台同步到云端”,以此实现离线可用。
不适用于
- 少量简单的配置信息(用 localStorage 更方便)
- 需要同步读写的场景(IndexedDB 是异步 API,没有同步版本,不能阻塞主线程)
- 需要跨域共享数据(同源策略限制)
- 需要存储敏感信息(IndexedDB 虽可存,但安全性不如加密后的 Cookie 或存放在后端)
基础操作:从打开数据库到读写数据
IndexedDB 的操作流程略微繁琐,需要先打开数据库,然后创建对象存储(类似数据库中的“表”),最后通过事务进行数据操作。下面展示最基础的使用模式。
1. 打开 / 创建数据库
// 打开名为 'MyDatabase' 的数据库,版本号为 1
const request = indexedDB.open('MyDatabase', 1);
request.onerror = (event) => {
console.error('数据库打开失败', event.target.error);
};
request.onsuccess = (event) => {
const db = event.target.result;
console.log('数据库打开成功', db);
// 之后的操作都在这里进行
};
// 首次创建或版本升级时触发
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 创建对象存储(相当于“建表”),设置主键
if (!db.objectStoreNames.contains('notes')) {
const store = db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true });
// 创建索引,可按 title 搜索、按 createdAt 排序
store.createIndex('titleIndex', 'title', { unique: false });
store.createIndex('createdIndex', 'createdAt', { unique: false });
}
};
onupgradeneeded 只在数据库不存在或版本号高于现有版本时触发,这是定义数据结构的最佳时机。之后 onsuccess 拿到 db 实例,就可以开始读写。
2. 添加数据
function addNote(db, note) {
// 开启一个读写事务,指定对象存储名称
const transaction = db.transaction(['notes'], 'readwrite');
const store = transaction.objectStore('notes');
const request = store.add(note);
request.onsuccess = () => console.log('笔记添加成功');
request.onerror = (e) => console.error('添加失败', e.target.error);
}
任何读写操作都必须包裹在事务里,readwrite 表示可读写。多个事务可以并发执行,但修改同一对象存储的事务会排队。
3. 查询数据(按主键)
function getNote(db, id) {
const transaction = db.transaction(['notes'], 'readonly');
const store = transaction.objectStore('notes');
const request = store.get(id);
request.onsuccess = (e) => {
if (request.result) {
console.log('查询结果:', request.result);
} else {
console.log('未找到该笔记');
}
};
}
4. 通过索引查询
function getNotesByTitle(db, title) {
const transaction = db.transaction(['notes'], 'readonly');
const store = transaction.objectStore('notes');
const index = store.index('titleIndex');
const request = index.getAll(title); // 获取所有匹配的笔记
request.onsuccess = (e) => {
console.log('标题匹配的笔记:', request.result);
};
}
5. 更新与删除
function updateNote(db, updatedNote) {
const transaction = db.transaction(['notes'], 'readwrite');
const store = transaction.objectStore('notes');
store.put(updatedNote); // 如果 keyPath 已存在则更新,否则新增
}
function deleteNote(db, id) {
const transaction = db.transaction(['notes'], 'readwrite');
const store = transaction.objectStore('notes');
store.delete(id);
}
基础操作小结
- IndexedDB 所有操作都是异步的,依靠事件或回调获取结果,不会阻塞主线程。
- 最核心的概念是数据库 → 对象存储 → 索引,一个数据库可以有多个对象存储,每个存储可以建多个索引。
- 事务确保数据一致性,读写操作必须明确指定事务模式和涉及的对象存储。
- 虽然原生 API 略显繁琐,但实际项目中常使用 Dexie.js、idb 这类轻量封装库,以 Promise 和链式调用的方式简化代码。
结合前面提到的 localStorage 和 Cookie,三种存储方案的能力边界一目了然:Cookie 适合少量身份信息且需随请求发送;localStorage 适合简单键值对持久化;而 IndexedDB 则是当你需要用“数据库的方式”在浏览器里管理数据时的唯一选择。