IndexedDB 完全指南:从基础使用到封装实战(含完整 API 与最佳实践)
在前端开发中,本地存储是实现数据持久化、提升用户体验的核心技术之一。常见的浏览器存储方案各有优劣,而 IndexedDB 作为一种高性能的本地数据库,凭借其大容量、异步操作、支持复杂数据类型等特性,成为处理大量结构化数据的首选方案。本文将系统讲解 IndexedDB 的核心概念、基础用法、完整封装以及最佳实践,弥补基础用法的遗漏点,帮助开发者快速上手并灵活运用。
一、前端存储方案对比(补充细节)
在深入 IndexedDB 之前,先明确它与其他存储方案的差异,方便根据场景选型:
| 存储方案 | 存储容量 | 时效性 | 数据类型 | 核心特性 | 适用场景 |
|---|---|---|---|---|---|
| LocalStorage | 5MB-10MB(浏览器差异) | 永久存储(手动清除) | 仅字符串(需序列化) | 同步操作,简单键值对 | 少量用户配置、token 存储 |
| SessionStorage | 5MB-10MB(浏览器差异) | 会话级(页面关闭清除) | 仅字符串(需序列化) | 同步操作,页面隔离 | 临时表单数据、会话状态 |
| Cookie | 4KB | 可设置过期时间(默认会话) | 仅字符串 | 随请求携带,同域共享 | 用户身份标识、跟踪统计 |
| IndexedDB | 无固定上限(依赖设备存储空间,通常 >250MB) | 永久存储(手动清除) | 字符串、数字、对象、二进制数据(Blob/ArrayBuffer) | 异步操作,事务支持,索引查询 | 大量结构化数据、离线应用、文件缓存 |
关键补充:
- LocalStorage/SessionStorage 同步操作会阻塞主线程,处理大量数据时可能导致页面卡顿;IndexedDB 异步操作不会阻塞 UI,性能更优。
- Cookie 每次请求都会携带到服务器,增加带宽消耗;IndexedDB 仅在本地操作,不与服务器交互。
- IndexedDB 支持事务(Transaction),确保数据操作的原子性(要么全部成功,要么全部失败),这是其他存储方案不具备的核心优势。
二、IndexedDB 核心概念(补充基础认知)
在使用前需理解以下核心术语,避免混淆:
- 数据库(Database) :IndexedDB 的顶层容器,每个数据库有唯一名称和版本号,版本号升级时会触发
onupgradeneeded事件。 - 对象仓库(Object Store) :类似关系型数据库的"表",用于存储结构化数据,每个对象仓库有唯一主键(keyPath)。
- 事务(Transaction) :所有数据操作(增删查改)必须通过事务执行,支持
readonly(只读)和readwrite(读写)两种模式,确保数据一致性。 - 索引(Index) :基于对象仓库的某个属性创建,用于快速查询数据(类似数据库索引),支持唯一索引(
unique: true)和非唯一索引。 - 主键(KeyPath) :对象仓库中每条数据的唯一标识,可手动指定(如
deviceId)或自动生成(autoIncrement: true)。 - 游标(Cursor) :用于遍历对象仓库中的数据,支持条件筛选、排序等复杂查询(基础用法中未提及,下文补充)。
三、IndexedDB 基础用法(完善遗漏 API 与场景)
1. 环境兼容处理
不同浏览器对 IndexedDB 的前缀支持不同,需先做兼容处理(补充完整前缀):
javascript
// 完整兼容方案
const indexedDB = window.indexedDB ||
window.webkitIndexedDB ||
window.mozIndexedDB ||
window.msIndexedDB; // IE 浏览器支持
const IDBTransaction = window.IDBTransaction ||
window.webkitIDBTransaction ||
window.mozIDBTransaction;
const IDBCursor = window.IDBCursor ||
window.webkitIDBCursor ||
window.mozIDBCursor;
// 检测浏览器是否支持
if (!indexedDB) {
console.error('当前浏览器不支持 IndexedDB,请升级浏览器');
}
2. 打开/创建数据库
使用 indexedDB.open(dbName, version) 打开数据库,版本号必须为正整数,升级版本时会触发 onupgradeneeded 事件(补充错误处理细节):
javascript
// 打开数据库(数据库名:deviceDB,版本号:2)
const request = indexedDB.open('deviceDB', 2);
// 数据库打开失败(如权限不足、存储满)
request.onerror = (event) => {
console.error('数据库打开失败:', event.target.error.message);
};
// 数据库打开成功
request.onsuccess = (event) => {
const db = event.target.result;
console.log('数据库打开成功,版本号:', db.version);
// 操作完成后关闭数据库(避免资源占用)
// db.close();
};
// 数据库创建或版本升级时触发(仅一次)
request.onupgradeneeded = (event) => {
const db = event.target.result;
console.log('数据库升级,旧版本:', event.oldVersion, '新版本:', event.newVersion);
// 此处可执行建表、删表、创建索引等操作
};
3. 操作对象仓库(补充完整场景)
(1)创建对象仓库(表)
在 onupgradeneeded 事件中创建对象仓库,支持两种主键模式(补充自动生成主键示例):
javascript
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 方式1:手动指定主键(如 deviceId)
if (!db.objectStoreNames.contains('cameraDevice')) {
const cameraStore = db.createObjectStore('cameraDevice', {
keyPath: 'deviceId' // 主键字段
});
// 创建索引:基于 deviceName(非唯一)、status(唯一)
cameraStore.createIndex('idx_deviceName', 'deviceName', { unique: false });
cameraStore.createIndex('idx_status', 'status', { unique: false });
}
// 方式2:自动生成主键(autoIncrement: true)
if (!db.objectStoreNames.contains('user')) {
const userStore = db.createObjectStore('user', {
autoIncrement: true // 主键自动递增(默认字段名:id)
});
userStore.createIndex('idx_username', 'username', { unique: true }); // 用户名唯一索引
}
// 删除旧表(如需)
if (db.objectStoreNames.contains('oldDevice')) {
db.deleteObjectStore('oldDevice');
console.log('旧表已删除');
}
};
(2)新增数据(补充批量新增与重复主键处理)
通过 add() 方法新增数据,主键重复会报错 ;若需覆盖重复数据,可使用 put() 方法(后续讲解):
javascript
// 数据库打开成功后执行新增
request.onsuccess = (event) => {
const db = event.target.result;
// 开启读写事务(指定操作的表名)
const transaction = db.transaction(['cameraDevice'], 'readwrite');
const cameraStore = transaction.objectStore('cameraDevice');
// 单个新增
cameraStore.add({
deviceId: 'cam_888',
deviceName: '成都匝道摄像头',
status: 1, // 1:在线,0:离线
resolution: '1080P',
createTime: new Date().toISOString()
});
// 批量新增(补充)
const batchData = [
{ deviceId: 'cam_999', deviceName: '北京路口摄像头', status: 1, resolution: '4K', createTime: new Date().toISOString() },
{ deviceId: 'cam_777', deviceName: '上海隧道摄像头', status: 0, resolution: '720P', createTime: new Date().toISOString() }
];
batchData.forEach(data => cameraStore.add(data));
// 事务完成回调
transaction.oncomplete = () => {
console.log('数据新增成功');
db.close(); // 关闭数据库
};
// 事务失败回调(如主键重复)
transaction.onerror = (event) => {
console.error('数据新增失败:', event.target.error.message);
db.close();
};
};
(3)查询数据(补充游标查询、条件筛选)
IndexedDB 支持主键查询、索引查询、全量查询和游标查询,满足不同场景需求:
ini
request.onsuccess = (event) => {
const db = event.target.result;
const transaction = db.transaction(['cameraDevice'], 'readonly');
const cameraStore = transaction.objectStore('cameraDevice');
// 1. 主键查询(精准匹配)
const keyRequest = cameraStore.get('cam_888');
keyRequest.onsuccess = () => {
console.log('主键查询结果:', keyRequest.result);
};
// 2. 索引查询(基于索引字段匹配)
const indexRequest = cameraStore.index('idx_deviceName').get('北京路口摄像头');
indexRequest.onsuccess = () => {
console.log('索引查询结果:', indexRequest.result);
};
// 3. 全量查询(获取所有数据)
const allRequest = cameraStore.getAll();
allRequest.onsuccess = () => {
console.log('全量查询结果:', allRequest.result);
};
// 4. 游标查询(补充:遍历数据、条件筛选、排序)
const cursorRequest = cameraStore.openCursor(null, 'next'); // next:正序,prev:倒序
const onlineCameras = [];
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
// 条件筛选:只保留在线设备(status: 1)
if (cursor.value.status === 1) {
onlineCameras.push(cursor.value);
}
cursor.continue(); // 继续遍历下一条
} else {
console.log('游标查询(在线设备):', onlineCameras);
}
};
transaction.oncomplete = () => {
db.close();
};
};
(4)修改数据(补充批量修改、索引定位修改)
使用 put() 方法修改数据,存在主键则更新,不存在则新增 (与 add() 的核心区别):
ini
request.onsuccess = (event) => {
const db = event.target.result;
const transaction = db.transaction(['cameraDevice'], 'readwrite');
const cameraStore = transaction.objectStore('cameraDevice');
// 方式1:主键定位修改
const getRequest = cameraStore.get('cam_777');
getRequest.onsuccess = () => {
const data = getRequest.result;
data.status = 1; // 状态改为在线
data.resolution = '1080P'; // 更新分辨率
cameraStore.put(data); // 提交修改
};
// 方式2:索引定位修改(补充)
const indexGetRequest = cameraStore.index('idx_deviceName').get('成都匝道摄像头');
indexGetRequest.onsuccess = () => {
const data = indexGetRequest.result;
data.createTime = new Date().toISOString(); // 更新时间
cameraStore.put(data);
};
transaction.oncomplete = () => {
console.log('数据修改成功');
db.close();
};
};
(5)删除数据(补充批量删除、索引定位删除)
支持主键删除和索引定位删除,批量删除需结合游标实现:
ini
request.onsuccess = (event) => {
const db = event.target.result;
const transaction = db.transaction(['cameraDevice'], 'readwrite');
const cameraStore = transaction.objectStore('cameraDevice');
// 1. 主键删除
cameraStore.delete('cam_999');
// 2. 索引定位删除(补充)
const indexGetRequest = cameraStore.index('idx_deviceName').get('上海隧道摄像头');
indexGetRequest.onsuccess = () => {
const data = indexGetRequest.result;
if (data) {
cameraStore.delete(data.deviceId); // 通过主键删除
}
};
// 3. 批量删除(补充:删除所有离线设备)
const cursorRequest = cameraStore.openCursor();
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
if (cursor.value.status === 0) {
cursor.delete(); // 删除当前游标指向的数据
}
cursor.continue();
}
};
transaction.oncomplete = () => {
console.log('数据删除成功');
db.close();
};
};
(6)清空表与删除数据库(补充)
ini
// 1. 清空表数据
request.onsuccess = (event) => {
const db = event.target.result;
const transaction = db.transaction(['cameraDevice'], 'readwrite');
const cameraStore = transaction.objectStore('cameraDevice');
cameraStore.clear(); // 清空表
transaction.oncomplete = () => {
console.log('表数据清空成功');
db.close();
};
};
// 2. 删除数据库(需先关闭所有连接)
const deleteRequest = indexedDB.deleteDatabase('deviceDB');
deleteRequest.onsuccess = () => {
console.log('数据库删除成功');
};
deleteRequest.onerror = (event) => {
console.error('数据库删除失败:', event.target.error.message);
};
四、IndexedDB 封装(优化健壮性与易用性)
基础用法存在代码冗余、错误处理繁琐等问题,下面封装一个通用的 IndexedDB 工具类,支持 Promise 链式调用,简化开发:
封装后的 IndexedDB 工具类(indexedDB.js)
javascript
/**
* IndexedDB 工具类(优化版)
* 支持 Promise 链式调用、自动兼容、事务管理、完整 API
*/
class IndexedDB {
constructor(dbName, dbVersion = 1) {
this.dbName = dbName;
this.dbVersion = dbVersion;
this.db = null; // 数据库实例
this.supported = !!window.indexedDB; // 检测浏览器支持
// 兼容前缀
this.indexedDB = window.indexedDB ||
window.webkitIndexedDB ||
window.mozIndexedDB ||
window.msIndexedDB;
this.IDBTransaction = window.IDBTransaction ||
window.webkitIDBTransaction ||
window.mozIDBTransaction;
}
/**
* 初始化数据库(创建表、索引)
* @param {Array} tableConfigs 表配置:[{ tableName, keyPath, autoIncrement, indexList }]
* @returns {Promise}
*/
init(tableConfigs = []) {
if (!this.supported) {
return Promise.reject(new Error('当前浏览器不支持 IndexedDB'));
}
return new Promise((resolve, reject) => {
const request = this.indexedDB.open(this.dbName, this.dbVersion);
// 数据库打开成功
request.onsuccess = (event) => {
this.db = event.target.result;
console.log(`数据库 ${this.dbName} 打开成功,版本号:${this.db.version}`);
resolve(this.db);
};
// 数据库打开失败
request.onerror = (event) => {
reject(new Error(`数据库打开失败:${event.target.error.message}`));
};
// 数据库创建/升级
request.onupgradeneeded = (event) => {
const db = event.target.result;
console.log(`数据库升级:旧版本 ${event.oldVersion} → 新版本 ${event.newVersion}`);
// 创建表和索引
tableConfigs.forEach(({ tableName, keyPath, autoIncrement = false, indexList = [] }) => {
if (!db.objectStoreNames.contains(tableName)) {
// 配置主键
const storeOptions = keyPath
? { keyPath, autoIncrement }
: { autoIncrement: true }; // 无 keyPath 时自动生成主键 id
const objectStore = db.createObjectStore(tableName, storeOptions);
// 创建索引
indexList.forEach(({ indexName, propName, unique = false }) => {
objectStore.createIndex(indexName, propName, { unique });
});
console.log(`表 ${tableName} 创建成功`);
}
});
resolve(db);
};
});
}
/**
* 开启事务
* @param {String|Array} tableNames 表名(单个或多个)
* @param {String} mode 事务模式:readonly / readwrite
* @returns {Object} objectStore 实例
*/
_transaction(tableNames, mode = 'readonly') {
if (!this.db) {
throw new Error('数据库未初始化,请先调用 init 方法');
}
const transaction = this.db.transaction(tableNames, mode);
// 事务失败处理
transaction.onerror = (event) => {
throw new Error(`事务失败:${event.target.error.message}`);
};
// 支持单个表名直接返回 objectStore,多个表名返回事务实例
return Array.isArray(tableNames) && tableNames.length > 1
? transaction
: transaction.objectStore(Array.isArray(tableNames) ? tableNames[0] : tableNames);
}
/**
* 新增数据
* @param {String} tableName 表名
* @param {Object|Array} data 单个数据对象或数组
* @returns {Promise}
*/
add(tableName, data) {
return new Promise((resolve, reject) => {
try {
const objectStore = this._transaction(tableName, 'readwrite');
const isBatch = Array.isArray(data);
// 批量新增
if (isBatch) {
data.forEach(item => objectStore.add(item));
} else {
objectStore.add(data);
}
// 事务完成
objectStore.transaction.oncomplete = () => {
resolve(isBatch ? `批量新增 ${data.length} 条数据成功` : '数据新增成功');
};
// 事务失败
objectStore.transaction.onerror = (event) => {
reject(new Error(`数据新增失败:${event.target.error.message}`));
};
} catch (error) {
reject(error);
}
});
}
/**
* 查询数据
* @param {String} tableName 表名
* @param {Object} options 查询配置:{ key, indexName, indexValue, cursorFilter }
* @returns {Promise}
*/
get(tableName, options = {}) {
return new Promise((resolve, reject) => {
try {
const objectStore = this._transaction(tableName);
const { key, indexName, indexValue, cursorFilter } = options;
let request;
// 1. 主键查询
if (key !== undefined) {
request = objectStore.get(key);
}
// 2. 索引查询
else if (indexName && indexValue !== undefined) {
request = objectStore.index(indexName).get(indexValue);
}
// 3. 游标查询(支持筛选)
else if (cursorFilter && typeof cursorFilter === 'function') {
request = objectStore.openCursor();
const result = [];
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
if (cursorFilter(cursor.value)) {
result.push(cursor.value);
}
cursor.continue();
} else {
resolve(result);
}
};
return;
}
// 4. 全量查询
else {
request = objectStore.getAll();
}
// 普通查询结果处理
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = (event) => {
reject(new Error(`数据查询失败:${event.target.error.message}`));
};
} catch (error) {
reject(error);
}
});
}
/**
* 修改数据
* @param {String} tableName 表名
* @param {Object} data 要修改的数据(必须包含主键)
* @returns {Promise}
*/
update(tableName, data) {
return new Promise((resolve, reject) => {
try {
const objectStore = this._transaction(tableName, 'readwrite');
const request = objectStore.put(data); // 存在则更新,不存在则新增
request.onsuccess = () => {
resolve('数据修改成功');
};
request.onerror = (event) => {
reject(new Error(`数据修改失败:${event.target.error.message}`));
};
} catch (error) {
reject(error);
}
});
}
/**
* 删除数据
* @param {String} tableName 表名
* @param {String|Number} key 主键值
* @returns {Promise}
*/
delete(tableName, key) {
return new Promise((resolve, reject) => {
try {
const objectStore = this._transaction(tableName, 'readwrite');
const request = objectStore.delete(key);
request.onsuccess = () => {
resolve('数据删除成功');
};
request.onerror = (event) => {
reject(new Error(`数据删除失败:${event.target.error.message}`));
};
} catch (error) {
reject(error);
}
});
}
/**
* 清空表数据
* @param {String} tableName 表名
* @returns {Promise}
*/
clear(tableName) {
return new Promise((resolve, reject) => {
try {
const objectStore = this._transaction(tableName, 'readwrite');
const request = objectStore.clear();
request.onsuccess = () => {
resolve('表数据清空成功');
};
request.onerror = (event) => {
reject(new Error(`表数据清空失败:${event.target.error.message}`));
};
} catch (error) {
reject(error);
}
});
}
/**
* 删除数据库
* @returns {Promise}
*/
deleteDatabase() {
return new Promise((resolve, reject) => {
if (this.db) {
this.db.close(); // 先关闭数据库连接
}
const request = this.indexedDB.deleteDatabase(this.dbName);
request.onsuccess = () => {
resolve(`数据库 ${this.dbName} 删除成功`);
};
request.onerror = (event) => {
reject(new Error(`数据库删除失败:${event.target.error.message}`));
};
});
}
/**
* 关闭数据库连接
*/
close() {
if (this.db) {
this.db.close();
this.db = null;
console.log(`数据库 ${this.dbName} 已关闭`);
}
}
}
export default IndexedDB;
工具类使用示例
javascript
// 1. 初始化数据库
import IndexedDB from './indexedDB';
// 配置表结构
const tableConfigs = [
{
tableName: 'cameraDevice',
keyPath: 'deviceId', // 主键
autoIncrement: false,
indexList: [
{ indexName: 'idx_deviceName', propName: 'deviceName', unique: false },
{ indexName: 'idx_status', propName: 'status', unique: false }
]
},
{
tableName: 'user',
autoIncrement: true, // 自动生成主键 id
indexList: [
{ indexName: 'idx_username', propName: 'username', unique: true }
]
}
];
// 创建数据库实例(数据库名:deviceDB,版本号:2)
const db = new IndexedDB('deviceDB', 2);
// 初始化并操作数据
db.init(tableConfigs)
.then(() => {
// 2. 新增数据
return db.add('cameraDevice', {
deviceId: 'cam_1001',
deviceName: '广州大桥摄像头',
status: 1,
resolution: '4K',
createTime: new Date().toISOString()
});
})
.then((msg) => {
console.log(msg);
// 3. 查询数据(游标筛选在线设备)
return db.get('cameraDevice', {
cursorFilter: (item) => item.status === 1
});
})
.then((onlineCameras) => {
console.log('在线设备:', onlineCameras);
// 4. 修改数据
return db.update('cameraDevice', {
deviceId: 'cam_1001',
deviceName: '广州大桥摄像头',
status: 1,
resolution: '8K', // 更新分辨率
createTime: new Date().toISOString()
});
})
.then((msg) => {
console.log(msg);
// 5. 删除数据
return db.delete('cameraDevice', 'cam_1001');
})
.then((msg) => {
console.log(msg);
// 6. 关闭数据库
db.close();
})
.catch((error) => {
console.error('操作失败:', error.message);
db.close();
});
五、最佳实践与注意事项(补充关键细节)
-
版本号管理:数据库版本号必须为正整数,升级后无法回退,修改表结构时需递增版本号。
-
事务生命周期:事务会在操作完成后自动提交,若长时间不操作(如超过 5 秒)会被浏览器终止,建议操作完成后立即关闭数据库。
-
错误处理:所有操作都需捕获错误(如主键重复、权限不足、存储满),避免影响页面正常运行。
-
性能优化:
- 批量操作时尽量合并为一个事务,减少事务创建次数。
- 大量数据查询使用游标(Cursor),避免使用
getAll()导致内存占用过高。 - 合理创建索引,提升查询效率,但避免过多索引(会影响新增/修改性能)。
-
数据序列化 :虽然 IndexedDB 支持存储对象,但复杂对象(如函数、循环引用对象)无法存储,需提前序列化(如
JSON.stringify)。 -
浏览器兼容性 :主流浏览器(Chrome、Firefox、Edge、Safari 10.1+)均支持 IndexedDB,如需兼容旧浏览器(如 IE9-),需使用
localForage等兼容库。 -
安全限制 :IndexedDB 受同源策略限制,不同域名无法访问彼此的数据库;本地文件(
file://协议)无法使用 IndexedDB,需通过服务器(http:///https://)访问。
六、总结
IndexedDB 作为前端高性能本地数据库,适用于需要存储大量结构化数据、实现离线功能的场景(如离线应用、数据缓存、本地日志存储等)。本文从存储方案对比、核心概念、基础用法、完整封装到最佳实践,全面覆盖了 IndexedDB 的关键知识点,弥补了基础用法的遗漏(如游标查询、批量操作、错误处理),封装的工具类简化了开发流程,提升了代码复用性。