7.2 IndexedDB 基础
7.2 IndexedDB 基础
IndexedDB是一种底层API,用于在客户端存储大量结构化数据。与Web Storage相比,IndexedDB提供了更强大的查询能力、事务支持和更大的存储空间(通常为浏览器可用磁盘空间的50%),适合构建需要本地持久化复杂数据的Web应用。
核心概念解析
1. 数据库架构
- Database:顶层容器,每个源(协议+域名+端口)可以创建多个数据库
- Object Store:相当于关系型数据库的表,存储键值对
- Index:在对象存储上创建的辅助索引,支持高效查询
- Transaction:原子操作单元,确保数据一致性
- Cursor:遍历对象存储或索引的机制
2. 与关系型数据库术语对照
| IndexedDB | 关系型数据库 |
|---|---|
| 数据库(Database) | 数据库(Database) |
| 对象存储(Object Store) | 表(Table) |
| 索引(Index) | 索引(Index) |
| 游标(Cursor) | 结果集(ResultSet) |
基础操作指南
1. 打开/创建数据库
const request = indexedDB.open('MyDatabase', 2); // 名称+版本号
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('customers')) {
const store = db.createObjectStore('customers', {
keyPath: 'id', // 主键
autoIncrement: true // 自增ID
});
// 创建索引
store.createIndex('name_idx', 'name', { unique: false });
store.createIndex('email_idx', 'email', { unique: true });
}
// 版本升级时可修改结构
if (event.oldVersion < 2) {
const store = event.currentTarget.transaction.objectStore('customers');
store.createIndex('age_idx', 'age', { unique: false });
}
};
2. CRUD操作
添加数据:
function addCustomer(db, customer) {
const transaction = db.transaction(['customers'], 'readwrite');
const store = transaction.objectStore('customers');
const request = store.add(customer);
request.onsuccess = () => {
console.log('客户数据已添加');
};
request.onerror = (event) => {
console.error('添加失败:', event.target.error);
};
}
查询数据:
function getCustomer(db, id) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['customers'], 'readonly');
const store = transaction.objectStore('customers');
const request = store.get(id);
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = (event) => {
reject(event.target.error);
};
});
}
// 使用索引查询
function getCustomerByName(db, name) {
const transaction = db.transaction(['customers'], 'readonly');
const store = transaction.objectStore('customers');
const index = store.index('name_idx');
const request = index.get(name);
request.onsuccess = () => {
console.log('查询结果:', request.result);
};
}
更新数据:
function updateCustomer(db, customer) {
const transaction = db.transaction(['customers'], 'readwrite');
const store = transaction.objectStore('customers');
const request = store.put(customer); // put方法自动更新
request.onsuccess = () => {
console.log('数据已更新');
};
}
删除数据:
function deleteCustomer(db, id) {
const transaction = db.transaction(['customers'], 'readwrite');
const store = transaction.objectStore('customers');
const request = store.delete(id);
request.onsuccess = () => {
console.log('数据已删除');
};
}
高级查询技术
1. 使用游标遍历数据
function getAllCustomers(db) {
const transaction = db.transaction(['customers'], 'readonly');
const store = transaction.objectStore('customers');
const customers = [];
const request = store.openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
customers.push(cursor.value);
cursor.continue();
} else {
console.log('获取所有客户:', customers);
}
};
}
2. 范围查询
// 查询年龄在18-30之间的客户
function getCustomersByAgeRange(db, min, max) {
const transaction = db.transaction(['customers'], 'readonly');
const store = transaction.objectStore('customers');
const index = store.index('age_idx');
const range = IDBKeyRange.bound(min, max);
const customers = [];
const request = index.openCursor(range);
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
customers.push(cursor.value);
cursor.continue();
} else {
console.log('年龄范围内的客户:', customers);
}
};
}
3. 复合查询模拟
// 通过多个索引筛选数据
function queryCustomers(db, conditions) {
return new Promise((resolve) => {
const transaction = db.transaction(['customers'], 'readonly');
const store = transaction.objectStore('customers');
const request = store.openCursor();
const results = [];
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
const customer = cursor.value;
let match = true;
// 手动检查每个条件
for (const [key, value] of Object.entries(conditions)) {
if (customer[key] !== value) {
match = false;
break;
}
}
if (match) results.push(customer);
cursor.continue();
} else {
resolve(results);
}
};
});
}
事务管理
1. 事务基础使用
function transferFunds(db, fromId, toId, amount) {
const transaction = db.transaction(['accounts'], 'readwrite');
const store = transaction.objectStore('accounts');
// 获取源账户
const fromRequest = store.get(fromId);
fromRequest.onsuccess = () => {
const fromAccount = fromRequest.result;
if (fromAccount.balance < amount) {
throw new Error('余额不足');
}
// 获取目标账户
const toRequest = store.get(toId);
toRequest.onsuccess = () => {
const toAccount = toRequest.result;
// 更新余额
fromAccount.balance -= amount;
toAccount.balance += amount;
// 保存更新
store.put(fromAccount);
store.put(toAccount);
};
};
transaction.oncomplete = () => {
console.log('转账完成');
};
transaction.onerror = (event) => {
console.error('转账失败:', event.target.error);
};
}
2. 事务隔离与模式
IndexedDB支持三种事务模式:
readonly:只读,可并发执行readwrite:读写,避免与其他读写事务同时执行versionchange:数据库结构变更
// 并发事务示例
function concurrentOperations(db) {
// 这两个事务可以并发执行
const readTx1 = db.transaction(['products'], 'readonly');
const readTx2 = db.transaction(['products'], 'readonly');
// 这个事务会等待前面的读写事务完成
const writeTx = db.transaction(['products'], 'readwrite');
}
实用封装示例
1. 通用数据库操作类
class IDBHelper {
constructor(dbName, version, upgradeCallback) {
this.dbPromise = new Promise((resolve, reject) => {
const request = indexedDB.open(dbName, version);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
upgradeCallback(event.target.result, event.oldVersion);
};
});
}
async get(storeName, key) {
const db = await this.dbPromise;
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
const request = store.get(key);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
async getAll(storeName, indexName, range) {
const db = await this.dbPromise;
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
const target = indexName ? store.index(indexName) : store;
const request = target.getAll(range);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
// 其他方法类似实现...
}
// 使用示例
const dbHelper = new IDBHelper('MyAppDB', 1, (db, oldVersion) => {
if (!db.objectStoreNames.contains('todos')) {
db.createObjectStore('todos', { keyPath: 'id', autoIncrement: true });
}
});
async function addTodo(todo) {
const db = await dbHelper.dbPromise;
return new Promise((resolve, reject) => {
const tx = db.transaction('todos', 'readwrite');
const store = tx.objectStore('todos');
const request = store.add(todo);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
性能优化策略
-
批量操作优化:
async function bulkAdd(storeName, items) { const db = await dbHelper.dbPromise; const tx = db.transaction(storeName, 'readwrite'); const store = tx.objectStore(storeName); items.forEach(item => { store.add(item); }); return new Promise((resolve, reject) => { tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } -
游标分批处理:
function processLargeDataset(db, batchSize, processCallback) { const transaction = db.transaction(['largeData'], 'readonly'); const store = transaction.objectStore('largeData'); let processed = 0; function processBatch(cursor) { let count = 0; while (cursor && count < batchSize) { processCallback(cursor.value); count++; processed++; cursor.continue(); } if (cursor) { console.log(`已处理 ${processed} 条记录`); // 使用setTimeout避免阻塞主线程 setTimeout(() => { cursor.continue(); }, 0); } } store.openCursor().onsuccess = (event) => { processBatch(event.target.result); }; } -
索引设计原则:
- 为常用查询条件创建索引
- 避免过多索引影响写入性能
- 对唯一性字段设置unique约束
错误处理与调试
-
常见错误类型:
ConstraintError:违反唯一约束TransactionInactiveError:事务已结束QuotaExceededError:超出存储配额
-
调试技巧:
// 添加全面的错误处理 transaction.onerror = (event) => { console.error('事务错误:', event.target.error.name, event.target.error.message); // 特定错误处理 if (event.target.error.name === 'QuotaExceededError') { showStorageFullWarning(); } }; -
浏览器开发者工具:
- Chrome: Application → IndexedDB
- Firefox: Storage → IndexedDB
- Edge: Debugger → IndexedDB
现代封装库推荐
-
Dexie.js:
const db = new Dexie('FriendsDB'); db.version(1).stores({ friends: '++id, name, age' }); async function addFriend() { await db.friends.add({ name: '张三', age: 25, }); } -
localForage:
localforage.config({ driver: [localforage.INDEXEDDB, localforage.WEBSQL, localforage.LOCALSTORAGE], name: 'MyApp' }); localforage.setItem('key', 'value').then(() => { return localforage.getItem('key'); }).then(value => { console.log(value); });
IndexedDB作为浏览器端最强大的存储方案之一,虽然API较为底层,但通过合理封装和现代工具库的帮助,完全可以成为开发复杂Web应用的可靠数据存储层。掌握其核心概念和最佳实践,能够显著提升应用的离线能力和响应速度。
#前端开发
分享于 2025-05-20