告别LocalStorage!用IndexedDB为你的Web App打造一个真正的本地数据库(附完整CRUD示例)
告别LocalStorage!用IndexedDB为你的Web App打造一个真正的本地数据库
前端开发者们对LocalStorage再熟悉不过了——这个简单的键值存储API让我们能够在浏览器中保存少量数据。但当应用复杂度提升,LocalStorage的局限性就暴露无遗:同步操作阻塞主线程、仅支持字符串存储、5MB容量限制...这时候,是时候认识一下IndexedDB这个真正的浏览器端数据库了。
IndexedDB不是新技术,但很多开发者对它望而却步,认为API复杂难用。实际上,一旦掌握其核心概念,你会发现它比想象中简单得多。本文将带你从LocalStorage的痛点出发,通过一个完整的Todo应用示例,手把手教你如何用IndexedDB实现复杂数据管理。
1. 为什么LocalStorage不够用了?
LocalStorage在简单场景下表现优异:保存用户偏好设置、存储临时表单数据等。但当你的应用需要处理以下情况时,它就显得力不从心:
- 数据量超过5MB:现代Web应用常需要缓存大量数据以实现离线功能
- 非字符串数据:存储JSON对象需要手动序列化和反序列化
- 复杂查询:无法按非键字段搜索,全表扫描性能极差
- 事务支持:批量操作中出错无法回滚
- 二进制数据:无法直接存储Blob、ArrayBuffer等类型
性能对比测试(1000条记录操作):
| 操作类型 | LocalStorage | IndexedDB |
|---|---|---|
| 写入1000条记录 | 320ms | 80ms |
| 读取1000条记录 | 280ms | 65ms |
| 条件查询 | 全表扫描 | 索引查询 |
提示:即使是中小型应用,当数据关系复杂时,IndexedDB的性能优势也会非常明显
2. IndexedDB核心概念快速入门
2.1 数据库架构
IndexedDB采用经典的数据库概念体系:
- Database:顶级容器,每个源(origin)可创建多个
- ObjectStore:相当于SQL中的表,存储键值对
- Index:在ObjectStore上创建的辅助索引
- Transaction:原子操作单元,确保数据一致性
- Cursor:遍历数据的迭代器
// 数据库初始化示例 const request = indexedDB.open('TodoDB', 1); request.onupgradeneeded = (event) => { const db = event.target.result; // 创建tasks表,主键为id const store = db.createObjectStore('tasks', { keyPath: 'id', autoIncrement: true }); // 创建created_at索引 store.createIndex('by_date', 'created_at', { unique: false }); };2.2 异步API设计
与LocalStorage的同步API不同,IndexedDB所有操作都是异步的:
const transaction = db.transaction('tasks', 'readwrite'); const store = transaction.objectStore('tasks'); const addRequest = store.add({ title: '学习IndexedDB', completed: false, created_at: new Date() }); addRequest.onsuccess = () => { console.log('任务添加成功'); }; addRequest.onerror = (event) => { console.error('添加失败:', event.target.error); };关键点:
- 每个操作返回一个Request对象
- 通过事件监听处理结果
- 错误必须显式处理
3. 实战:Todo应用的完整CRUD实现
让我们构建一个功能完整的Todo应用,涵盖IndexedDB的所有基础操作。
3.1 数据库初始化
首先封装一个通用的数据库连接方法:
class TodoDB { constructor(name, version) { this.db = null; this.request = indexedDB.open(name, version); this.request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains('tasks')) { const store = db.createObjectStore('tasks', { keyPath: 'id', autoIncrement: true }); store.createIndex('by_status', 'completed', { unique: false }); store.createIndex('by_date', 'created_at', { unique: false }); } }; this.request.onsuccess = (event) => { this.db = event.target.result; this.onReady?.(); }; } withTransaction(storeName, mode, callback) { const tx = this.db.transaction(storeName, mode); const store = tx.objectStore(storeName); return callback(store, tx); } }3.2 实现CRUD操作
创建任务:
async addTask(task) { return new Promise((resolve, reject) => { this.withTransaction('tasks', 'readwrite', (store) => { const request = store.add({ ...task, created_at: new Date(), updated_at: new Date() }); request.onsuccess = () => resolve(request.result); request.onerror = (event) => reject(event.target.error); }); }); }读取任务:
async getTask(id) { return new Promise((resolve, reject) => { this.withTransaction('tasks', 'readonly', (store) => { const request = store.get(id); request.onsuccess = () => resolve(request.result); request.onerror = (event) => reject(event.target.error); }); }); }更新任务:
async updateTask(id, updates) { return new Promise((resolve, reject) => { this.withTransaction('tasks', 'readwrite', async (store) => { // 先获取当前数据 const getRequest = store.get(id); getRequest.onsuccess = () => { const data = getRequest.result; const updated = { ...data, ...updates, updated_at: new Date() }; const putRequest = store.put(updated); putRequest.onsuccess = () => resolve(); putRequest.onerror = (event) => reject(event.target.error); }; getRequest.onerror = (event) => reject(event.target.error); }); }); }删除任务:
async deleteTask(id) { return new Promise((resolve, reject) => { this.withTransaction('tasks', 'readwrite', (store) => { const request = store.delete(id); request.onsuccess = () => resolve(); request.onerror = (event) => reject(event.target.error); }); }); }3.3 高级查询功能
按状态筛选任务:
async getTasksByStatus(completed) { return new Promise((resolve, reject) => { this.withTransaction('tasks', 'readonly', (store) => { const index = store.index('by_status'); const request = index.getAll(IDBKeyRange.only(completed)); request.onsuccess = () => resolve(request.result); request.onerror = (event) => reject(event.target.error); }); }); }日期范围查询:
async getTasksByDateRange(startDate, endDate) { return new Promise((resolve, reject) => { this.withTransaction('tasks', 'readonly', (store) => { const index = store.index('by_date'); const range = IDBKeyRange.bound(startDate, endDate); const request = index.getAll(range); request.onsuccess = () => resolve(request.result); request.onerror = (event) => reject(event.target.error); }); }); }4. 性能优化与最佳实践
4.1 批量操作技巧
使用事务批量处理数据可以显著提升性能:
async bulkAddTasks(tasks) { return new Promise((resolve, reject) => { this.withTransaction('tasks', 'readwrite', (store) => { const results = []; let completed = 0; tasks.forEach((task, i) => { const request = store.add({ ...task, created_at: new Date() }); request.onsuccess = () => { results[i] = request.result; if (++completed === tasks.length) { resolve(results); } }; request.onerror = (event) => { reject(event.target.error); }; }); }); }); }4.2 索引设计原则
- 选择性高的字段优先:如状态字段只有true/false,索引效果有限
- 复合索引:对经常一起查询的字段创建复合索引
- 避免过度索引:每个索引会增加写入开销
4.3 错误处理策略
IndexedDB错误处理容易被忽视,建议:
- 事务级错误:监听transaction.onerror
- 请求级错误:每个request都需要onerror处理
- 全局错误:监听window.onerror捕获未处理的异常
// 健壮的错误处理示例 this.withTransaction('tasks', 'readwrite', (store, tx) => { tx.onerror = (event) => { console.error('事务失败:', event.target.error); // 可以考虑重试逻辑 }; const request = store.add(task); request.onerror = (event) => { console.error('添加失败:', event.target.error); // 特定错误的处理逻辑 if (event.target.error.name === 'ConstraintError') { // 处理唯一约束冲突 } }; });5. 从LocalStorage迁移到IndexedDB
对于已有项目,平滑迁移是关键。以下是推荐步骤:
双写阶段:
- 新数据同时写入LocalStorage和IndexedDB
- 优先从IndexedDB读取,失败则回退到LocalStorage
数据迁移:
- 首次加载时检查LocalStorage中是否有旧数据
- 批量导入到IndexedDB
- 迁移成功后标记,避免重复迁移
逐步替换:
- 按功能模块逐步替换LocalStorage调用
- 保留LocalStorage清理逻辑,作为回滚方案
async migrateFromLocalStorage() { const legacyData = localStorage.getItem('todo-app'); if (!legacyData) return; try { const tasks = JSON.parse(legacyData); await this.bulkAddTasks(tasks); localStorage.removeItem('todo-app'); console.log('迁移成功'); } catch (error) { console.error('迁移失败:', error); // 可以添加重试机制或用户提示 } }在实际项目中,IndexedDB的版本管理需要特别注意。每次数据库结构变更都应增加版本号,并在onupgradeneeded中处理升级逻辑。对于大型应用,可以考虑使用类似Dexie.js这样的封装库来简化开发。
