🔥 IndexedDB 从入门到实战:前端本地大容量存储解决方案
🚀 一、IndexedDB 核心介绍
1. 什么是 IndexedDB?
IndexedDB 是浏览器提供的本地非关系型(NoSQL)数据库,属于前端核心存储方案之一,专为解决传统前端存储(Cookie、LocalStorage)的容量限制和功能短板设计,支持大容量、结构化数据的本地持久化存储,是前端离线应用、数据缓存、本地业务数据存储的核心技术。

2. 核心特性
- ✅ 大容量存储:无统一官方容量上限,由浏览器根据设备存储空间动态分配(通常数GB级别),远优于 LocalStorage(5MB 左右)和 Cookie(4KB 左右);
- ✅ 非关系型(NoSQL) :以键值对为核心存储结构,支持存储对象、数组等结构化数据(无需手动序列化,浏览器自动处理);
- ✅ 异步操作:所有核心操作(增删改查、库/表创建)均为异步,不会阻塞浏览器主线程,避免页面卡顿,这是与 LocalStorage(同步)的关键区别;
- ✅ 事务支持:所有数据操作必须在事务中执行,保证操作的原子性(要么全部成功,要么全部失败),避免数据不一致;
- ✅ 索引支持:可对对象仓库的指定字段创建索引,大幅提升数据查询效率,解决海量数据下的查询性能问题;
- ✅ 持久化存储:数据默认持久化,除非手动删除或清除浏览器缓存,关闭页面/浏览器后数据不会丢失;
- ✅ 跨域限制:遵循同源策略,仅能访问当前域名下创建的 IndexedDB 数据库,保证数据安全性。
3. 与传统前端存储的对比
| 特性 | IndexedDB | LocalStorage | Cookie |
|---|---|---|---|
| 存储容量 | 大(数GB级别) | 小(约5MB) | 极小(约4KB) |
| 操作方式 | 异步 | 同步 | 同步 |
| 数据类型 | 支持结构化数据(对象/数组) | 仅字符串 | 仅字符串 |
| 事务支持 | 支持 | 不支持 | 不支持 |
| 索引支持 | 支持 | 不支持 | 不支持 |
| 持久化 | 永久(手动清除才丢失) | 永久 | 可设置过期时间 |
| 主线程阻塞 | 不会 | 会(大数据操作卡顿) | 会 |
| 服务端交互 | 不主动携带 | 不主动携带 | 每次请求自动携带 |
🎯 二、IndexedDB 核心概念
IndexedDB 的操作模型围绕数据库、对象仓库、索引、事务展开,核心概念与传统数据库对应但更轻量化,先理解概念再上手操作,能大幅降低学习成本:
- 数据库(Database):前端域名下的独立数据容器,一个域名可创建多个数据库,每个数据库有唯一名称,是所有数据操作的根节点;
- 对象仓库(Object Store) :等价于传统数据库的表,是存储结构化数据的核心载体,一个数据库可包含多个对象仓库,数据以键值对形式存储在其中;
- 主键(Primary Key) :对象仓库的唯一标识字段,类似数据库的主键,保证每条数据的唯一性,支持自增主键 (由浏览器自动生成唯一ID)和显式主键(指定自定义字段作为主键);
- 索引(Index):基于对象仓库的非主键字段创建的查询优化结构,可对单个字段或多个字段创建索引,支持快速根据索引字段查询数据,避免全库遍历;
- 事务(Transaction) :IndexedDB 所有数据操作的唯一执行载体 ,任何增删改查、对象仓库操作都必须在事务中进行。事务分为三种模式:
readonly:只读模式(查询数据),性能最高,支持多个只读事务同时执行;readwrite:读写模式(增删改数据),同一时间仅能有一个该模式事务执行;versionchange:版本变更模式(创建/删除对象仓库、创建索引),用于数据库版本升级,是唯一能修改数据库结构的模式。
- 数据库版本 :IndexedDB 有严格的版本管理机制,版本号为正整数 (仅支持升级,不支持降级),数据库结构的修改(如增删对象仓库、创建索引)仅能在版本升级时通过
versionchange事务完成。
📁 三、IndexedDB 完整使用流程(原生 API)
IndexedDB 原生 API 基于事件和回调实现(也可封装为 Promise/async-await 简化使用),以下以学生信息管理为例,讲解从数据库创建、数据操作到索引查询的完整流程,所有代码可直接在浏览器控制台/前端页面中运行。
前置:封装 Promise 版 API(简化异步操作)
原生 IndexedDB 回调嵌套较多,先封装为 Promise 版本,后续使用async-await更简洁,核心方法全覆盖:
javascript
/**
* IndexedDB Promise 封装工具类
* @param {string} dbName - 数据库名称
* @param {number} version - 数据库版本(正整数)
* @param {Function} upgradeCb - 版本升级回调(创建/删除对象仓库、索引)
*/
class IndexedDBUtil {
constructor(dbName, version, upgradeCb) {
this.dbName = dbName;
this.version = version;
this.upgradeCb = upgradeCb;
this.db = null; // 数据库实例
}
// 打开数据库(核心方法)
open() {
return new Promise((resolve, reject) => {
// 浏览器兼容性判断
if (!window.indexedDB) {
reject(new Error('你的浏览器不支持 IndexedDB,请升级至现代浏览器'));
return;
}
// 打开数据库,版本号不匹配时触发upgradeneeded事件
const request = indexedDB.open(this.dbName, this.version);
// 版本升级/首次创建时触发(仅在版本号大于当前版本时执行)
request.onupgradeneeded = (e) => {
this.db = e.target.result;
this.upgradeCb && this.upgradeCb(this.db); // 执行自定义升级逻辑
console.log('数据库版本升级,当前版本:', e.target.transaction.db.version);
};
// 数据库打开成功
request.onsuccess = (e) => {
this.db = e.target.result;
resolve(this.db);
console.log('数据库打开成功');
};
// 数据库打开失败
request.onerror = (e) => {
reject(new Error('数据库打开失败:' + e.target.error.message));
};
// 数据库被意外关闭
request.onblocked = () => {
reject(new Error('数据库操作被阻塞,请关闭其他标签页后重试'));
};
});
}
// 开启事务
/**
* @param {Array<string>} storeNames - 涉及的对象仓库名称数组
* @param {string} mode - 事务模式:readonly/readwrite/versionchange
* @returns {IDBTransaction} 事务实例
*/
transaction(storeNames, mode = 'readonly') {
if (!this.db) throw new Error('数据库未打开,请先调用open方法');
return this.db.transaction(storeNames, mode);
}
// 新增/修改数据(主键存在则修改,不存在则新增)
/**
* @param {string} storeName - 对象仓库名称
* @param {Object} data - 要存储的数据
*/
put(storeName, data) {
return new Promise((resolve, reject) => {
try {
const tx = this.transaction([storeName], 'readwrite');
const store = tx.objectStore(storeName);
const request = store.put(data);
request.onsuccess = () => resolve('操作成功,主键:' + request.result);
request.onerror = (e) => reject(new Error('增改失败:' + e.target.error.message));
} catch (err) {
reject(err);
}
});
}
// 根据主键查询单条数据
get(storeName, key) {
return new Promise((resolve, reject) => {
try {
const tx = this.transaction([storeName]);
const store = tx.objectStore(storeName);
const request = store.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = (e) => reject(new Error('查询失败:' + e.target.error.message));
} catch (err) {
reject(err);
}
});
}
// 查询所有数据
getAll(storeName) {
return new Promise((resolve, reject) => {
try {
const tx = this.transaction([storeName]);
const store = tx.objectStore(storeName);
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = (e) => reject(new Error('查询所有失败:' + e.target.error.message));
} catch (err) {
reject(err);
}
});
}
// 根据主键删除数据
delete(storeName, key) {
return new Promise((resolve, reject) => {
try {
const tx = this.transaction([storeName], 'readwrite');
const store = tx.objectStore(storeName);
const request = store.delete(key);
request.onsuccess = () => resolve('删除成功');
request.onerror = (e) => reject(new Error('删除失败:' + e.target.error.message));
} catch (err) {
reject(err);
}
});
}
// 清空对象仓库所有数据
clear(storeName) {
return new Promise((resolve, reject) => {
try {
const tx = this.transaction([storeName], 'readwrite');
const store = tx.objectStore(storeName);
const request = store.clear();
request.onsuccess = () => resolve('清空成功');
request.onerror = (e) => reject(new Error('清空失败:' + e.target.error.message));
} catch (err) {
reject(err);
}
});
}
// 根据索引查询数据
/**
* @param {string} storeName - 对象仓库名称
* @param {string} indexName - 索引名称
* @param {*} value - 索引字段匹配值
*/
getByIndex(storeName, indexName, value) {
return new Promise((resolve, reject) => {
try {
const tx = this.transaction([storeName]);
const store = tx.objectStore(storeName);
const index = store.index(indexName); // 获取索引
const request = index.get(value); // 根据索引值查询
request.onsuccess = () => resolve(request.result);
request.onerror = (e) => reject(new Error('索引查询失败:' + e.target.error.message));
} catch (err) {
reject(err);
}
});
}
// 关闭数据库
close() {
if (this.db) {
this.db.close();
this.db = null;
console.log('数据库已关闭');
}
}
// 删除整个数据库
deleteDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.deleteDatabase(this.dbName);
request.onsuccess = () => {
this.db = null;
resolve('数据库删除成功');
};
request.onerror = (e) => reject(new Error('数据库删除失败:' + e.target.error.message));
});
}
}
步骤1:初始化数据库,创建对象仓库和索引
创建名为studentDB的数据库,版本号1,创建student对象仓库(存储学生信息),并为phone(手机号)字段创建唯一索引 (保证手机号不重复)、为className(班级)字段创建普通索引(支持按班级查询)。
核心规则:
- 自增主键:通过
{ autoIncrement: true }设置,浏览器自动生成唯一数字ID(主键名默认为id); - 唯一索引:
{ unique: true },保证索引字段值不重复; - 普通索引:
{ unique: false },支持字段值重复。
javascript
// 1. 初始化数据库工具类,定义版本升级逻辑(创建对象仓库和索引)
const dbUtil = new IndexedDBUtil(
'studentDB', // 数据库名称
1, // 版本号
(db) => { // 版本升级回调(首次创建/版本升级时执行)
// 判断对象仓库是否已存在,避免重复创建
if (!db.objectStoreNames.contains('student')) {
// 创建student对象仓库,设置自增主键
const studentStore = db.createObjectStore('student', {
keyPath: 'id', // 主键字段
autoIncrement: true // 自增主键
});
// 为phone字段创建唯一索引,索引名:idx_phone
studentStore.createIndex('idx_phone', 'phone', { unique: true });
// 为className字段创建普通索引,索引名:idx_className
studentStore.createIndex('idx_className', 'className', { unique: false });
console.log('对象仓库和索引创建成功');
}
}
);
// 2. 打开数据库(必须执行,否则无法进行后续操作)
await dbUtil.open();
步骤2:核心数据操作(增、删、改、查)
所有操作基于上述封装的工具类,使用async-await简化异步流程,操作对象为student对象仓库,数据结构为:{ id: 自增ID, name: 姓名, age: 年龄, phone: 手机号, className: 班级 }。
2.1 新增数据(put 方法)
主键(id)由浏览器自动生成,直接传入其他字段即可:
javascript
// 新增单条学生数据
const addRes1 = await dbUtil.put('student', {
name: '张三',
age: 18,
phone: '13800138000',
className: '高一(1)班'
});
console.log(addRes1); // 输出:操作成功,主键:1
// 批量新增(循环调用put即可)
const students = [
{ name: '李四', age: 17, phone: '13900139000', className: '高一(1)班' },
{ name: '王五', age: 18, phone: '13700137000', className: '高一(2)班' }
];
for (const s of students) {
const res = await dbUtil.put('student', s);
console.log(res); // 依次输出:操作成功,主键:2、操作成功,主键:3
}
2.2 根据主键查询单条数据(get 方法)
传入对象仓库名称和主键值(id),返回匹配的整条数据:
javascript
const student = await dbUtil.get('student', 1);
console.log('主键为1的学生:', student);
// 输出:{ id: 1, name: '张三', age: 18, phone: '13800138000', className: '高一(1)班' }
2.3 查询所有数据(getAll 方法)
返回对象仓库中的所有数据,返回结果为数组:
javascript
const allStudents = await dbUtil.getAll('student');
console.log('所有学生信息:', allStudents);
// 输出:[{id:1,...}, {id:2,...}, {id:3,...}]
2.4 修改数据(put 方法)
IndexedDB 中修改和新增共用 put 方法 ,只需传入包含主键的完整数据,浏览器会根据主键匹配并覆盖原有数据:
javascript
// 修改主键为1的学生信息(修改年龄和班级)
const updateRes = await dbUtil.put('student', {
id: 1, // 必须传入主键,否则会新增一条数据
name: '张三', // 未修改的字段也需传入,否则会被覆盖为undefined
age: 19,
phone: '13800138000',
className: '高二(1)班'
});
console.log(updateRes); // 输出:操作成功,主键:1
// 验证修改结果
const updatedStudent = await dbUtil.get('student', 1);
console.log('修改后的学生信息:', updatedStudent);
// 输出:{ id:1, name:'张三', age:19, phone:'13800138000', className:'高二(1)班' }
2.5 根据主键删除数据(delete 方法)
传入对象仓库名称和主键值,删除对应数据:
javascript
const delRes = await dbUtil.delete('student', 3);
console.log(delRes); // 输出:删除成功
// 验证删除结果
const allStudentsAfterDel = await dbUtil.getAll('student');
console.log('删除后的所有学生:', allStudentsAfterDel); // 仅保留id=1、2的学生
2.6 清空对象仓库(clear 方法)
删除对象仓库中的所有数据,但保留对象仓库和索引结构(与删除数据库不同):
javascript
// 注意:执行后所有学生数据会被清空,谨慎使用
const clearRes = await dbUtil.clear('student');
console.log(clearRes); // 输出:清空成功
步骤3:索引查询(核心优化手段)
如果直接根据非主键字段(如phone、className)查询,原生需要遍历所有数据,效率极低;通过索引查询可直接定位数据,海量数据下性能提升显著。
以下使用步骤1中创建的两个索引,演示索引查询的使用:
javascript
// 1. 根据唯一索引idx_phone查询(手机号查询)
const studentByPhone = await dbUtil.getByIndex('student', 'idx_phone', '13900139000');
console.log('手机号为13900139000的学生:', studentByPhone);
// 输出:{ id:2, name:'李四', age:17, phone:'13900139000', className:'高一(1)班' }
// 2. 根据普通索引idx_className查询(班级查询)
// 注:封装的getByIndex返回单条匹配数据,若需多条可扩展封装index.getAll(value)
const studentByClass = await dbUtil.getByIndex('student', 'idx_className', '高一(1)班');
console.log('高一(1)班的学生:', studentByClass);
步骤4:数据库高级操作(关闭、删除、版本升级)
4.1 关闭数据库
使用完成后关闭数据库,释放资源:
javascript
dbUtil.close(); // 输出:数据库已关闭
4.2 删除整个数据库
删除数据库后,所有对象仓库、索引、数据都会被清除,谨慎使用:
javascript
const delDBRes = await dbUtil.deleteDB();
console.log(delDBRes); // 输出:数据库删除成功
4.3 数据库版本升级(修改结构)
若需新增对象仓库、创建/删除索引,需升级版本号 (如从1升级为2),并在升级回调中编写结构修改逻辑,示例:新增teacher对象仓库,为student仓库新增email索引:
javascript
// 版本升级为2,新增teacher仓库和email索引
const dbUtilV2 = new IndexedDBUtil(
'studentDB', // 同一数据库名称
2, // 版本号必须大于之前的1
(db) => { // 新版本升级回调
const tx = db.transaction(db.objectStoreNames, 'versionchange');
// 1. 新增teacher对象仓库,自增主键
if (!db.objectStoreNames.contains('teacher')) {
const teacherStore = db.createObjectStore('teacher', {
keyPath: 'id',
autoIncrement: true
});
console.log('新增teacher对象仓库成功');
}
// 2. 为student仓库新增email普通索引
const studentStore = tx.objectStore('student');
if (!studentStore.indexNames.contains('idx_email')) {
studentStore.createIndex('idx_email', 'email', { unique: false });
console.log('新增idx_email索引成功');
}
}
);
// 打开升级后的数据库
await dbUtilV2.open();
版本升级核心规则:
- 版本号仅能递增(1→2→3,不能2→1);
- 若数据库已存在且版本号与当前一致,不会触发升级回调;
- 升级回调中可通过
db.objectStoreNames和store.indexNames判断对象仓库/索引是否存在,避免重复创建。
🚨 四、IndexedDB 实用开发建议
1. 兼容性处理
现代浏览器均支持 IndexedDB,但仍需对低版本浏览器做兼容判断(已在封装工具类中实现),兼容范围:
- Chrome ≥ 23、Firefox ≥ 10、Edge ≥ 12、Safari ≥ 7.1;
- 移动端:微信小程序/公众号、App 内置浏览器(基于 Chromium)均支持,IE 完全不支持。
2. 性能优化
- 尽量使用只读事务 (
readonly)做查询操作,避免占用读写事务资源; - 批量操作时,复用同一个读写事务,而非多次创建事务(减少事务开销);
- 对高频查询的非主键字段创建索引,避免全库遍历;
- 避免在主线程中执行超大批量数据操作,可拆分为多个小批次,防止页面卡顿。
3. 数据安全
- IndexedDB 遵循同源策略,跨域无法访问,无需担心数据泄露;
- 敏感数据(如用户密码、令牌)不建议存储 在 IndexedDB 中,可使用浏览器
sessionStorage(会话级存储)或加密后存储; - 清除浏览器缓存(如清除Cookie、本地存储)时,IndexedDB 数据会被一并删除,若需持久化核心数据,可结合后端云存储。
4. 错误处理
- 所有 IndexedDB 操作都需增加异常捕获 (
try-catch),处理数据库打开失败、事务阻塞、主键/索引重复等错误; - 处理
versionchange事务的阻塞问题(当其他标签页打开同一数据库时,版本升级会被阻塞),可提示用户关闭其他标签页后重试。
5. 替代方案(第三方库)
原生 IndexedDB API 略显繁琐,实际开发中可使用成熟的第三方封装库,简化开发:
- localForage:最流行的 IndexedDB 封装库,API 与 LocalStorage 一致,自动降级(不支持 IndexedDB 则使用 LocalStorage);
- Dexie.js:专为 IndexedDB 设计的轻量级库,支持链式调用、高级查询、事务管理,体积小(≈20KB);
- PouchDB:支持离线同步的 IndexedDB 库,可与后端 CouchDB 同步数据,适合离线应用。
五、适用场景
IndexedDB 适用于前端需要本地存储大容量、结构化数据的场景,典型应用:
- 离线应用:如离线文档、离线地图、离线电商App,在无网络时存储业务数据,联网后同步至后端;
- 数据缓存:缓存高频访问的接口数据(如商品列表、用户信息),减少网络请求,提升页面加载速度;
- 本地业务数据存储:如前端表单草稿、本地编辑的文档、购物车数据(无需实时同步至后端的场景);
- 海量本地数据管理:如前端日志收集、本地数据报表,需要存储数万条甚至更多结构化数据的场景。
不适用场景:
- 存储少量简单数据(如开关状态、用户昵称),直接使用 LocalStorage 更简洁;
- 需实时同步至后端的敏感数据(如支付信息、用户身份信息);
- 跨域数据共享(IndexedDB 不支持跨域)。
📌 六、总结
- IndexedDB 是浏览器端大容量、异步、支持事务和索引的非关系型本地数据库,是解决传统前端存储容量限制的核心方案;
- 核心概念:数据库、对象仓库(表)、主键、索引、事务(三种模式),数据库结构修改仅能在版本升级时完成;
- 原生 API 基于事件回调,可封装为 Promise/async-await 简化使用,实际开发中也可使用 localForage、Dexie.js 等第三方库;
- 所有数据操作必须在事务中执行,查询用只读事务,增删改用读写事务,结构修改用版本变更事务;
- 优势:大容量、异步无阻塞、支持索引和事务、持久化存储;短板:API 繁琐、版本管理严格、IE 不兼容;
- 适用场景:离线应用、数据缓存、本地大容量结构化数据存储,是前端工程化、离线化的重要技术支撑。
IndexedDB 作为前端本地存储的"终极方案",弥补了 LocalStorage 和 Cookie 的不足,掌握其核心使用方法,能大幅提升前端应用的离线能力和用户体验,是现代前端开发者的必备技能之一。