IndexedDB 完整介绍
IndexedDB 是浏览器内置的非关系型、事务型本地数据库 API,属于 Web Storage 体系的高级存储方案,保存在本机浏览器内部,持久化存储(关闭页面、重启浏览器数据仍保留)。
定位:替代 localStorage / sessionStorage,适合存储大量结构化数据,支持索引、查询、事务、游标;localStorage 仅适合少量字符串。
一、核心特性
- 异步 API:所有读写操作不会阻塞主线程(对比 localStorage 同步,大数据会卡页面)。
- 非关系型(NoSQL):存储对象,不是表+行;存储单元叫对象仓库 ObjectStore(类似数据库表),直接存 JS 对象,不需要定义固定字段。
- 事务支持:所有操作必须在事务内执行,保证原子性;读写失败自动回滚。
- 支持索引:可以给对象属性建立索引,实现快速查询,也支持主键查询。
- 存储容量大:容量配额由浏览器决定,一般可用磁盘空间的一部分,远大于 localStorage(通常 5MB)。
- 同源策略:严格同源,A网站不能读取B网站的 IndexedDB。
- 持久化:默认持久存储;但浏览器存储空间紧张时,可能清理(可申请持久化权限
navigator.storage.persist())。 - 不支持 SQL:没有 SQL 查询语句,靠主键、索引、游标遍历数据。
限制:只能在浏览器环境(主线程 / WebWorker)使用,Node.js 原生没有 IndexedDB。
二、基础概念名词
| 名词 | 说明 |
|---|---|
| Database(数据库) | 一个域名下可以创建多个独立数据库,每个库有名称 + 版本号 |
| ObjectStore 对象仓库 | 数据库里的“表”,存放一组 JS 对象;每个仓库必须指定主键(keyPath 内联主键 / autoIncrement 自增) |
| Index 索引 | 在对象某个属性上建立索引,用于按该字段检索数据;索引可唯一/非唯一 |
| Transaction 事务 | 读写操作容器,模式:readonly只读、readwrite读写;事务有作用范围(绑定若干ObjectStore) |
| Request 请求 | IndexedDB 所有操作返回 IDBRequest 对象,通过 onsuccess / onerror 监听结果 |
| Cursor 游标 | 遍历对象仓库/索引里多条数据,分页、批量迭代 |
| KeyRange 键范围 | 限定查询主键区间:大于、小于、区间匹配 |
三、版本升级机制(重点)
IndexedDB 通过版本号控制结构变更:
- 打开数据库
indexedDB.open(dbName, version) - 如果版本 > 当前本地版本:触发
onupgradeneeded,只能在这里创建/删除对象仓库、创建索引 - 版本号只能增大,不能降级;如果打开版本低于本地库版本,直接报错
VersionError
结构修改(增删表、索引)只能在升级回调中执行,不能在普通读写事务里修改库结构。
四、简单示例代码(原生 API)
// 打开数据库,库名myDB,版本1
const openReq = indexedDB.open('myDB', 1);
// 数据库升级:首次创建 / 版本提升时执行
openReq.onupgradeneeded = (e) => {
const db = e.target.result;
// 创建对象仓库,主键是id
const store = db.createObjectStore('users', { keyPath: 'id' });
// 在name字段建立索引,非唯一
store.createIndex('nameIndex', 'name', { unique: false });
};
openReq.onsuccess = (e) => {
const db = e.target.result;
// 新增数据
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
store.put({ id: 1, name: '张三', age: 22 });
};
openReq.onerror = e => console.error('打开失败', e.target.error);
五、读写操作:add / put / get / delete / clear
add(obj):新增,主键已存在则报错put(obj):新增或覆盖更新;主键存在就覆盖,不存在插入(最常用)get(key):按主键读取单条delete(key):按主键删除clear():清空整个对象仓库所有数据getAll():一次性取出全部数据(大量数据慎用,占用内存)openCursor():游标遍历,适合大数据分页
六、存储配额与浏览器清理策略
配额说明
- 默认:尽力存储(best-effort)。磁盘紧张时,浏览器按 LRU(最近最少使用)策略,直接删除整个源(origin)的全部存储(IndexedDB + Cache等),不会只删部分数据。
- 申请持久存储:
navigator.storage.persist(),成功后不会被系统自动清理;但用户手动清除网站数据依然会删掉。 - 查询用量:
navigator.storage.estimate()返回usage(已用)、quota(配额上限)。 - 写入超限:抛出
QuotaExceededError。
Safari 特殊规则:网站7天无交互,自动清空站点存储;PWA添加到主屏幕不受此限制。
七、存储的数据类型
支持结构化克隆算法可序列化对象:
✅ 普通对象、数组、字符串、数字、布尔、Date、RegExp、Blob、File、ArrayBuffer、Map、Set
❌ 不能存:函数、DOM节点、循环引用对象(会报错)
八、IndexedDB vs localStorage vs CacheAPI
| API | 异步 | 存储类型 | 容量 | 查询能力 | 适用场景 |
|---|---|---|---|---|---|
| IndexedDB | ✅异步 | 对象/二进制 | 大 | 索引、游标、范围查询 | 大量结构化离线数据、表单、本地文档 |
| localStorage | ❌同步 | 仅字符串 | ~5MB | 仅key-value | 少量简单配置 |
| Cache API | ✅异步 | Request/Response | 大 | 按请求匹配 | PWA静态资源缓存 |
九、原生API痛点与封装库
原生 IndexedDB API 非常繁琐,大量回调嵌套,实际项目一般用封装库:
- idb(推荐,轻量,基于Promise封装,官方维护)
- localForage:底层可切换 IndexedDB / WebSQL / localStorage,简单 key-value 风格
- Dexie.js:功能更强,类SQL链式查询,适合复杂查询场景
import { openDB } from 'idb';
const db = await openDB('myDB', 1, {
upgrade(db) {
const store = db.createObjectStore('users', { keyPath: 'id' });
store.createIndex('nameIndex', 'name');
}
});
await db.add('users', {id:1, name:'张三'});
const user = await db.get('users',1);
idb 简单示例:
import { openDB } from 'idb';
const db = await openDB('myDB', 1, {
upgrade(db) {
const store = db.createObjectStore('users', { keyPath: 'id' });
store.createIndex('nameIndex', 'name');
}
});
await db.add('users', {id:1, name:'张三'});
const user = await db.get('users',1);
十、调试方式(浏览器)
Chrome / Edge:F12 → Application → IndexedDB,可以:
- 查看数据库、对象仓库、索引
- 查看、新增、修改、删除记录
- 删除数据库
注意:刷新页面不会清空 IndexedDB;清除网站数据才会删除。
十一、常见坑点
- 版本号只能升不能降,打开更低版本直接报错。
onupgradeneeded回调是唯一能创建/删除仓库和索引的地方,普通读写事务不允许改结构。- 事务自动提交:事务一旦没有任何未完成请求,会立刻自动提交;不要在事务里写异步await。
- 主键不能重复,
add重复主键报错,put会覆盖。 - 同源隔离:http 和 https、不同端口,属于不同 origin,数据库完全隔离。
- 大量数据一次性 getAll() 会占用内存,优先使用游标分页。
- 存储大 Blob/File:浏览器内部做引用管理,但是依然计入配额。
十二、适用场景 & 不适合场景
✅ 适合
- PWA离线应用,本地缓存大量业务数据
- 前端离线表单、离线文档编辑器
- 客户端大量结构化数据,需要检索、分页
- 图片/文件二进制本地缓存
❌ 不适合
- 简单少量配置(优先 localStorage)
- 服务端数据库替代(浏览器本地存储,用户可随意删除)
- 跨域共享数据(同源限制)
- 超大数据集(GB级别不推荐,浏览器性能下降)
如果你需要,我可以继续输出:
- IndexedDB 完整可运行原生 Promise 封装
- Dexie.js 增删改查+分页游标模板
- IndexedDB 迁移、备份导出导入方案
你想要哪一部分?
IndexedDB 在浏览器中存储和检索数据完整实操
IndexedDB 原生是回调风格,代码比较啰嗦;下面分两套方案:原生API(理解底层) + idb库(项目推荐,Promise封装),包含:打开库、建表、增、查、改、删、分页。
前置概念回顾
- 所有操作异步,基于请求对象
IDBRequest - 库结构变更(新建/删除对象仓库、索引)只能在
onupgradeneeded - 读写必须在事务
transaction中执行 - 同源隔离;数据保存在当前站点浏览器本地
方案一:原生 IndexedDB API(无第三方库)
1. 打开数据库,创建对象仓库(表)+索引
// 打开数据库:库名,版本号(版本升高触发升级)
function openDB() {
return new Promise((resolve, reject) => {
const req = indexedDB.open("demoDB", 1);
// 数据库升级:首次创建 OR 版本号提升时触发
req.onupgradeneeded = (ev) => {
const db = ev.target.result;
// 创建对象仓库 users,主键 keyPath = id
const userStore = db.createObjectStore("users", { keyPath: "id" });
// 创建索引:按 name 查询,非唯一索引
userStore.createIndex("name_idx", "name", { unique: false });
};
req.onsuccess = (ev) => resolve(ev.target.result);
req.onerror = (ev) => reject(ev.target.error);
});
}
2. 新增数据(add / put)
add: 主键不存在才插入,主键重复报错put: 存在则更新,不存在则新增(upsert)
async function addUser(db, user) {
return new Promise((resolve, reject) => {
// 创建读写事务,指定对象仓库
const tx = db.transaction("users", "readwrite");
const store = tx.objectStore("users");
const req = store.put(user); // upsert
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
// 使用示例
// const db = await openDB();
// await addUser(db, { id: 1, name: "小明", age: 20 });
// await addUser(db, { id: 2, name: "小红", age: 22 });
3. 检索数据
3.1 通过主键查询单条 get()
async function getUserById(db, id) {
return new Promise((resolve, reject) => {
const tx = db.transaction("users", "readonly");
const store = tx.objectStore("users");
const req = store.get(id);
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
// const user = await getUserById(db, 1);
3.2 通过索引查询(按name查找)
async function getUserByName(db, name) {
return new Promise((resolve, reject) => {
const tx = db.transaction("users", "readonly");
const store = tx.objectStore("users");
const index = store.index("name_idx");
const req = index.get(name); // 匹配第一条
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
3.3 获取全部数据 getAll()
数据量大不要用 getAll,会一次性加载全部进内存
async function getAllUsers(db) {
return new Promise((resolve, reject) => {
const tx = db.transaction("users", "readonly");
const store = tx.objectStore("users");
const req = store.getAll();
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
3.4 游标 Cursor 分页查询(大数据推荐)
/**
* 游标分页
* @param db
* @param pageSize 每页条数
* @param skip 跳过多少条
*/
async function getUsersByCursor(db, pageSize, skip = 0) {
return new Promise((resolve, reject) => {
const tx = db.transaction("users", "readonly");
const store = tx.objectStore("users");
const cursorReq = store.openCursor();
const result = [];
let count = 0;
cursorReq.onsuccess = (ev) => {
const cursor = ev.target.result;
if (!cursor) {
resolve(result);
return;
}
if (count >= skip && result.length < pageSize) {
result.push(cursor.value);
}
count++;
cursor.continue(); // 移动到下一条
};
cursorReq.onerror = (ev) => reject(ev.target.error);
});
}
// const list = await getUsersByCursor(db, 10, 0); // 第1页,10条
4. 更新数据
直接用 put,主键不变,字段覆盖:
// await addUser(db, { id:1, name:"小明", age:21 }); // 更新age
5. 删除数据
async function deleteUser(db, id) {
return new Promise((resolve, reject) => {
const tx = db.transaction("users", "readwrite");
const store = tx.objectStore("users");
const req = store.delete(id);
req.onsuccess = () => resolve(true);
req.onerror = () => reject(req.error);
});
}
6. 清空整张表
async function clearUsers(db) {
return new Promise((resolve, reject) => {
const tx = db.transaction("users", "readwrite");
const store = tx.objectStore("users");
const req = store.clear();
req.onsuccess = () => resolve(true);
req.onerror = () => reject(req.error);
});
}
7. 关闭数据库
// db.close();
方案二:idb 库(推荐生产使用,Promise封装)
idb 是官方推荐轻量封装库,消除回调地狱:https://github.com/jakearchibald/idb
1. 引入
<!-- CDN -->
<script type="module">
import { openDB } from '[https://cdn.jsdelivr.net/npm/idb@7/+esm](https://cdn.jsdelivr.net/npm/idb@7/+esm)';
</script>
2. 完整增删查改示例
import { openDB } from '[https://cdn.jsdelivr.net/npm/idb@7/+esm](https://cdn.jsdelivr.net/npm/idb@7/+esm)';
// 初始化数据库
const db = await openDB('demoDB', 1, {
upgrade(db) {
// 版本升级时执行
const store = db.createObjectStore('users', { keyPath: 'id' });
store.createIndex('name_idx', 'name');
},
});
// 新增/更新
await db.put('users', { id: 1, name: '小明', age: 20 });
// 主键查询
const user = await db.get('users', 1);
// 索引查询
const userByName = await db.getFromIndex('users', 'name_idx', '小明');
// 获取全部
const all = await db.getAll('users');
// 分页游标
const pageData = [];
let cursor = await db.transaction('users').store.openCursor();
let skip = 0, pageSize = 10;
while (cursor && pageData.length < pageSize) {
if (skip <= 0) pageData.push(cursor.value);
skip--;
cursor = await cursor.continue();
}
// 删除
await db.delete('users', 1);
// 清空
await db.clear('users');
浏览器调试查看数据
F12 → Application → IndexedDB → 选中你的数据库
✅ 查看对象仓库、索引、记录
✅ 手动增删改测试数据
✅ 删除数据库
关键注意事项(实操高频踩坑)
- 版本号只能增大,不能降级;如果你把版本从 2 改回 1,打开数据库直接报错。修改表结构必须升级版本。
onupgradeneeded是唯一可以创建/删除对象仓库和索引的地方,普通读写事务不能修改库结构。- 事务自动提交:事务中没有剩余请求时,事务自动结束;不要在事务回调内部写额外异步请求。
- 存储限制:同源配额,超出抛出
QuotaExceededError;可以用navigator.storage.estimate()查看占用。 - 可存储类型:支持结构化克隆(对象、Blob、ArrayBuffer、File);不能存函数、DOM节点、循环引用对象。
- 同源隔离:http/https、不同域名、不同端口数据库互相独立。
典型使用流程总结
indexedDB.open(dbName, version)打开数据库onupgradeneeded创建对象仓库、索引(首次/版本升级)- 获取 db 实例
- 创建事务
transaction(storeName, mode) - 获取对象仓库
objectStore() - 调用
get/put/add/delete/openCursor读写数据 - 通过
onsuccess/onerror获取结果
如果你想要,我可以给你:
- 可直接复制运行的完整 HTML 页面(带UI,输入表单保存+查询)
- IndexedDB 数据导出/备份、导入恢复代码
- 范围查询(KeyRange,大于、小于、区间筛选)示例
需要哪个?