浏览器本地存储方案:IndexedDB
在 Web 开发中,当我们需要存储比 localStorage 更大、更复杂的数据时,IndexedDB 是最佳选择。它本质上是一个运行在浏览器中的大容量本地数据库,支持异步操作、索引查询以及二进制文件存储。
核心对比与场景
| 特性 | localStorage | IndexedDB |
|---|---|---|
| 容量 | 约 5MB | 至少 250MB,甚至 GB 级 |
| 数据类型 | 仅字符串 | JS 对象、文件、二进制等 |
| 查询方式 | 简单键值对 | 支持索引、范围查询 |
| 执行模式 | 同步(阻塞) | 异步(不阻塞页面) |
什么时候用?
- 选 localStorage:存储 token、用户偏好设置等小数据,结构简单且量小于 5MB。
- 选 IndexedDB:离线应用的全部数据、用户生成的内容(如图片笔记)、大量 API 缓存或编辑器草稿自动保存。
注意:不要用它存密码等敏感信息(不安全),也不要替代服务器端实时同步数据(建议用 WebSocket)。
基础使用与封装
虽然原生 API 功能强大但较为繁琐,生产环境通常配合 idb 这样的轻量级库使用,它能提供 Promise 风格的接口。
1. 安装与初始化
IndexedDB 本身无需安装,这里指的是引入辅助库:
yarn add idb
2. 核心操作示例
打开数据库时会自动创建,若版本号变更则触发升级逻辑。以下是增删改查的完整流程:
import { openDB } from 'idb';
// 1. 打开/创建数据库
const db = await openDB('MyAppDB', 1, {
upgrade(db) {
// 仅在版本更新或初次创建时执行
// 创建对象仓库(相当于表),指定主键为 id 字段
db.createObjectStore('tasks', { keyPath: 'id' });
}
});
// 2. 添加或更新数据 (put)
await db.put('tasks', {
id: 1,
title: '学习 IDB',
completed: false
});
// 3. 查询所有数据
const allTasks = await db.getAll('tasks');
// 4. 根据主键查询单条
const task = await db.get('tasks', 1);
// 5. 删除数据
await db.delete('tasks', 1);
3. 工具类封装实践
为了复用性,建议将数据库操作封装为单例类。这样既能管理连接状态,又能统一错误处理。
import { openDB } from 'idb';
class TaskDB {
constructor() {
this.dbName = 'TaskDatabase';
this.storeName = 'tasks';
this.version = 1;
}
async connect() {
return openDB(this.dbName, this.version, {
upgrade(db) {
if (!db.objectStoreNames.contains('tasks')) {
db.createObjectStore('tasks', { keyPath: 'id' });
}
}
});
}
async saveTask(task) {
const db = await this.connect();
// put 方法:id 存在则更新,不存在则新增
return db.put(this.storeName, task);
}
async deleteTask(id) {
const db = await this.connect();
return db.delete(this.storeName, id);
}
async getAllTasks() {
const db = await this.connect();
return db.getAll(this.storeName);
}
async getTask(id) {
const db = await this.connect();
return db.get(this.storeName, id);
}
}
export default new TaskDB();
React 组件集成
在 React 中使用只需结合 useEffect 和 useState 即可实现数据持久化。下面是一个简单的任务管理器示例:
import React, { useState, useEffect } from 'react';
import taskDB from './taskDB';
function TaskManager() {
const [tasks, setTasks] = useState([]);
const [newTaskTitle, setNewTaskTitle] = useState('');
// 组件加载时从数据库读取
useEffect(() => {
loadTasks();
}, []);
const loadTasks = async () => {
const savedTasks = await taskDB.getAllTasks();
setTasks(savedTasks || []);
};
const handleAddTask = async () => {
if (!newTaskTitle.trim()) return;
const newTask = {
id: Date.now(),
title: newTaskTitle,
completed: false,
createdAt: new Date().toISOString()
};
await taskDB.saveTask(newTask);
setTasks([...tasks, newTask]);
setNewTaskTitle('');
};
const handleToggleTask = async (task) => {
const updatedTask = { ...task, completed: !task.completed };
await taskDB.saveTask(updatedTask);
setTasks(tasks.map(t => t.id === task.id ? updatedTask : t));
};
const handleDeleteTask = async (taskId) => {
await taskDB.deleteTask(taskId);
setTasks(tasks.filter(task => task.id !== taskId));
};
return (
<div className="task-manager">
<h1>任务管理器</h1>
<div className="add-task">
<input
type="text"
value={newTaskTitle}
onChange={(e) => setNewTaskTitle(e.target.value)}
placeholder="输入新任务..."
/>
<button onClick={handleAddTask}>添加</button>
</div>
<ul className="task-list">
{tasks.map(task => (
<li key={task.id} className={task.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={task.completed}
onChange={() => handleToggleTask(task)}
/>
<span>{task.title}</span>
<button onClick={() => handleDeleteTask(task.id)}>删除</button>
</li>
))}
</ul>
<div className="stats">
总计:{tasks.length} | 已完成:{tasks.filter(t => t.completed).length}
</div>
</div>
);
}
export default TaskManager;
关键概念与注意事项
在实际开发中,有几个细节需要特别注意:
- 主键设置:
keyPath: 'id'意味着每条数据必须有唯一的 id 字段,否则无法正确索引。 - put vs add:
put()允许更新已存在的记录,而add()只能新增,重复会报错。 - 版本控制:修改数据库结构(如新增索引)时,必须增加版本号并编写
upgrade逻辑。
// 版本升级示例:从 v1 升级到 v2,新增标题索引
openDB('MyDB', 2, {
upgrade(db, oldVersion) {
if (oldVersion < 2) {
const store = db.transaction.objectStore('tasks');
store.createIndex('title', 'title');
}
}
});
故障排查
如果遇到数据库操作失败,可以尝试以下检查:
- 异步问题:确保所有操作都加了
await,避免回调地狱或状态不同步。 - 兼容性:检查
window.indexedDB是否存在(IE 不支持)。 - 清除缓存:开发阶段可手动调用
indexedDB.deleteDatabase('MyAppDB')重置数据。
try {
const db = await openDB(...);
await db.put(...);
} catch (error) {
console.error('数据库错误:', error);
}
参考资料
- GitHub 仓库:jakearchibald/idb
- 在线体验:IDB 示例

