IndexedDB 是浏览器提供的客户端数据库,可以存储大量结构化数据,支持事务操作,是 Web 应用离线存储的重要技术。
1. IndexedDB 基础概念
核心特性
- 异步操作:所有操作都是异步的,避免阻塞主线程
- 事务支持:提供原子性保证
- 键值对存储:以对象存储为单位
- 索引支持:可创建索引提高查询效率
- 大容量存储:比 localStorage 大得多
基本工作流程
javascript
// 1. 打开数据库
const request = indexedDB.open('MyDatabase', 1);
// 2. 处理升级事件
request.onupgradeneeded = function(event) {
const db = event.target.result;
// 创建对象存储空间
if (!db.objectStoreNames.contains('users')) {
const objectStore = db.createObjectStore('users', { keyPath: 'id' });
objectStore.createIndex('name', 'name', { unique: false });
}
};
// 3. 处理成功/失败
request.onsuccess = function(event) {
const db = event.target.result;
// 进行数据操作
};
request.onerror = function(event) {
console.error('数据库打开失败:', event.target.error);
};
2. 封装 IndexedDB 工具类
javascript
class IndexedDBHelper {
constructor(dbName, version = 1) {
this.dbName = dbName;
this.version = version;
this.db = null;
}
// 打开数据库
open() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};
// 数据库升级
request.onupgradeneeded = (event) => {
this.db = event.target.result;
this.setupSchema(event);
};
});
}
// 创建数据库结构
setupSchema(event) {
const db = event.target.result;
// 创建用户表
if (!db.objectStoreNames.contains('users')) {
const userStore = db.createObjectStore('users', { keyPath: 'id' });
userStore.createIndex('email', 'email', { unique: true });
userStore.createIndex('name', 'name', { unique: false });
}
// 创建缓存表
if (!db.objectStoreNames.contains('cache')) {
const cacheStore = db.createObjectStore('cache', { keyPath: 'key' });
cacheStore.createIndex('expireTime', 'expireTime', { unique: false });
}
}
// 添加数据
add(storeName, data) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.add(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 更新数据
update(storeName, data) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.put(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 获取数据
get(storeName, key) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const request = store.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 查询数据
getAll(storeName) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 删除数据
delete(storeName, key) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.delete(key);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
// 清空表
clear(storeName) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.clear();
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
}
3. 实际应用示例
用户信息本地缓存
javascript
// 使用示例
const db = new IndexedDBHelper('UserApp', 1);
// 初始化数据库
async function initDB() {
try {
await db.open();
console.log('数据库初始化成功');
} catch (error) {
console.error('数据库初始化失败:', error);
}
}
// 缓存用户数据
async function cacheUserInfo(userInfo) {
try {
await db.add('users', userInfo);
console.log('用户信息缓存成功');
} catch (error) {
console.error('缓存失败:', error);
}
}
// 获取用户信息
async function getUserInfo(userId) {
try {
const user = await db.get('users', userId);
return user;
} catch (error) {
console.error('获取用户信息失败:', error);
return null;
}
}
// 搜索用户
async function searchUsers(searchTerm) {
try {
const allUsers = await db.getAll('users');
return allUsers.filter(user =>
user.name.includes(searchTerm) || user.email.includes(searchTerm)
);
} catch (error) {
console.error('搜索用户失败:', error);
return [];
}
}
数据缓存管理器
javascript
class CacheManager {
constructor() {
this.db = new IndexedDBHelper('CacheDB', 1);
this.init();
}
async init() {
await this.db.open();
}
// 设置缓存(带过期时间)
async set(key, value, expire = 3600000) { // 默认1小时过期
const cacheData = {
key,
value,
createTime: Date.now(),
expireTime: Date.now() + expire
};
await this.db.add('cache', cacheData);
}
// 获取缓存
async get(key) {
const cacheData = await this.db.get('cache', key);
if (!cacheData) return null;
// 检查是否过期
if (Date.now() > cacheData.expireTime) {
await this.remove(key);
return null;
}
return cacheData.value;
}
// 删除缓存
async remove(key) {
await this.db.delete('cache', key);
}
// 清理过期缓存
async cleanExpired() {
const allCache = await this.db.getAll('cache');
const now = Date.now();
const expiredKeys = allCache
.filter(item => item.expireTime < now)
.map(item => item.key);
for (const key of expiredKeys) {
await this.remove(key);
}
}
}
// 使用示例
const cache = new CacheManager();
// 设置缓存
await cache.set('api_data', { data: 'some data' }, 3600000);
// 获取缓存
const cachedData = await cache.get('api_data');
4. 性能优化技巧
批量操作优化
javascript
// 批量添加数据
async function batchAdd(storeName, dataArray) {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const promises = dataArray.map(data => {
return new Promise((resolve, reject) => {
const request = store.add(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
});
return Promise.all(promises);
}
// 批量更新
async function batchUpdate(storeName, dataArray) {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const promises = dataArray.map(data => {
return new Promise((resolve, reject) => {
const request = store.put(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
});
return Promise.all(promises);
}
索引优化
javascript
// 创建高效的索引
setupSchema(event) {
const db = event.target.result;
// 为常用查询字段创建索引
if (!db.objectStoreNames.contains('products')) {
const productStore = db.createObjectStore('products', { keyPath: 'id' });
productStore.createIndex('category', 'category', { unique: false });
productStore.createIndex('price', 'price', { unique: false });
productStore.createIndex('createdAt', 'createdAt', { unique: false });
productStore.createIndex('searchIndex', ['name', 'description'], { unique: false });
}
}
// 使用索引进行高效查询
async function searchProducts(category, minPrice) {
const transaction = this.db.transaction(['products'], 'readonly');
const store = transaction.objectStore('products');
const categoryIndex = store.index('category');
// 范围查询
const range = IDBKeyRange.bound(minPrice, Infinity);
const priceIndex = store.index('price');
// 注意:实际使用时需要更复杂的查询逻辑
const request = categoryIndex.getAll();
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
5. 错误处理和最佳实践
完整的错误处理
javascript
class RobustIndexedDB {
constructor(dbName, version = 1) {
this.dbName = dbName;
this.version = version;
this.db = null;
this.isSupported = this.checkSupport();
}
checkSupport() {
return !!window.indexedDB;
}
async safeOpen() {
if (!this.isSupported) {
throw new Error('IndexedDB not supported');
}
try {
const db = await this.open();
return db;
} catch (error) {
console.error('Database open failed:', error);
throw error;
}
}
async handleTransaction(operation) {
try {
return await operation();
} catch (error) {
console.error('Transaction failed:', error);
throw error;
}
}
}
版本管理
javascript
// 数据库版本升级处理
request.onupgradeneeded = function(event) {
const db = event.target.result;
// 根据不同版本执行不同的升级逻辑
switch (event.oldVersion) {
case 0:
// 第一次创建
createInitialStructure(db);
break;
case 1:
// 从版本1升级到版本2
upgradeFromV1ToV2(db);
break;
default:
// 其他版本处理
break;
}
};
function createInitialStructure(db) {
const userStore = db.createObjectStore('users', { keyPath: 'id' });
userStore.createIndex('email', 'email', { unique: true });
userStore.createIndex('name', 'name', { unique: false });
}
function upgradeFromV1ToV2(db) {
// 添加新字段
if (db.objectStoreNames.contains('users')) {
const userStore = db.transaction('users', 'readwrite').objectStore('users');
// 可以在这里添加新索引或修改结构
}
}
6. 使用建议
适用场景
- 大量数据存储:超过 5MB 的数据存储需求
- 离线应用:需要在没有网络时访问数据
- 复杂查询:需要通过索引进行高效查询
- 持久化缓存:需要长期保存用户数据
注意事项
- 异步编程:始终使用 Promise 或回调处理异步操作
- 事务管理:合理使用事务确保数据一致性
- 内存管理:及时关闭数据库连接,避免内存泄漏
- 兼容性检查:使用前检查浏览器支持情况
- 错误处理:完善的错误处理机制
监控和调试
javascript
// 添加调试信息
class DebugIndexedDB extends IndexedDBHelper {
constructor(dbName, version = 1) {
super(dbName, version);
this.debug = true;
}
async debugOperation(operationName, operation) {
if (this.debug) {
console.time(`[DB] ${operationName}`);
}
try {
const result = await operation();
if (this.debug) {
console.timeEnd(`[DB] ${operationName}`);
}
return result;
} catch (error) {
if (this.debug) {
console.timeEnd(`[DB] ${operationName}`);
console.error(`[DB] ${operationName} failed:`, error);
}
throw error;
}
}
}
通过以上封装和最佳实践,可以在前端项目中高效、安全地使用 IndexedDB,为用户提供更好的离线体验和数据持久化能力。