什么是 IDB?
IDB 是一个轻量级的 IndexedDB 包装库,它提供了基于 Promise 的 API,让 IndexedDB 的使用变得更加简单直观。相比于原生 IndexedDB 复杂的回调机制,IDB 提供了更现代化、更易用的接口。8.x 版本带来了更好的性能和更简洁的 API。
IDB 8.x 核心 API
1. 打开/创建数据库
javascript
import { openDB } from 'idb';
// 打开或创建数据库 - 推荐使用版本管理
const openDatabase = async () => {
return openDB('my-database', 2, {
upgrade(db, oldVersion) {
console.log(`Upgrading database from version ${oldVersion} to 2`);
// 版本迁移逻辑
if (oldVersion < 1) {
// 版本 0 到 1 的迁移
const store = db.createObjectStore('store1', {
keyPath: 'id',
autoIncrement: true
});
store.createIndex('name-index', 'name');
store.createIndex('age-index', 'age');
}
if (oldVersion < 2) {
// 版本 1 到 2 的迁移
const newStore = db.createObjectStore('store2', {
keyPath: 'uuid'
});
newStore.createIndex('category-index', 'category');
}
},
// 数据库被其他标签页阻塞时的处理
blocked(currentVersion, blockedVersion) {
console.warn(`Database is blocked by version ${blockedVersion}`);
// 可以提示用户关闭其他标签页
},
// 数据库连接终止时的处理
terminating() {
console.warn('Database connection is terminating');
},
// 数据库关闭时的处理
closed() {
console.log('Database connection closed');
}
});
};
// 使用示例
const db = await openDatabase();
2. 基本 CRUD 操作
javascript
// 添加数据 - 返回生成的ID
const addData = async () => {
const id = await db.add('store1', {
name: 'Alice',
age: 30,
createdAt: new Date()
});
return id;
};
// 读取数据
const getData = async (id) => {
const data = await db.get('store1', id);
return data;
};
// 更新数据 - put 方法会创建或更新
const updateData = async (id, updates) => {
const existing = await db.get('store1', id);
await db.put('store1', {
...existing,
...updates,
updatedAt: new Date()
});
};
// 删除数据
const deleteData = async (id) => {
await db.delete('store1', id);
};
// 计数 - 获取存储中的对象数量
const countData = async () => {
const count = await db.count('store1');
return count;
};
3. 事务操作
javascript
// 使用事务进行多个操作
const performTransaction = async () => {
const tx = db.transaction('store1', 'readwrite');
const store = tx.objectStore('store1');
try {
await store.add({ name: 'Bob', age: 25, createdAt: new Date() });
await store.add({ name: 'Charlie', age: 35, createdAt: new Date() });
await tx.done; // 确保事务完成
console.log('Transaction completed successfully');
} catch (error) {
console.error('Transaction failed:', error);
tx.abort(); // 显式中止事务
throw error;
}
};
// 使用事务获取多个对象
const getMultipleItems = async (keys) => {
const values = await db.getAll('store1', keys);
return values;
};
4. 高级查询操作
javascript
// 使用索引范围查询
const queryByAge = async (minAge) => {
const index = db.transaction('store1').store.index('age-index');
const adults = await index.getAll(IDBKeyRange.lowerBound(minAge));
return adults;
};
// 使用游标遍历 - 更高效的方式
const iterateWithCursor = async (callback) => {
const tx = db.transaction('store1', 'readonly');
const store = tx.objectStore('store1');
let cursor = await store.openCursor();
while (cursor) {
await callback(cursor.value);
cursor = await cursor.continue();
}
};
// 使用键范围进行复杂查询
const complexQuery = async () => {
const range = IDBKeyRange.bound(18, 65); // 年龄在18-65之间
const results = await db.getAll('store1', range);
return results;
};
在 React Hooks 项目中使用 IDB 8.x
1. 安装依赖
bash
npm install idb
2. 创建数据库配置
javascript
// lib/db.js
import { openDB } from 'idb';
// 数据库常量
export const DB_NAME = 'myAppDB';
export const DB_VERSION = 3;
export const STORE_NAMES = {
TODOS: 'todos',
NOTES: 'notes',
SETTINGS: 'settings'
};
// 数据库服务类
class DatabaseService {
constructor() {
this.db = null;
this.isInitializing = false;
}
// 初始化数据库
async init() {
if (this.db) return this.db;
if (this.isInitializing) {
// 如果已经在初始化,等待初始化完成
return new Promise((resolve) => {
const checkInit = () => {
if (this.db) {
resolve(this.db);
} else {
setTimeout(checkInit, 100);
}
};
checkInit();
});
}
this.isInitializing = true;
try {
this.db = await openDB(DB_NAME, DB_VERSION, {
upgrade: (db, oldVersion) => {
console.log(`Database upgrade from version ${oldVersion} to ${DB_VERSION}`);
// 版本迁移策略
if (oldVersion < 1) {
// 创建 todos 存储
const todoStore = db.createObjectStore(STORE_NAMES.TODOS, {
keyPath: 'id',
autoIncrement: true,
});
todoStore.createIndex('completed-index', 'completed');
todoStore.createIndex('createdAt-index', 'createdAt');
}
if (oldVersion < 2) {
// 创建 notes 存储
const noteStore = db.createObjectStore(STORE_NAMES.NOTES, {
keyPath: 'id',
autoIncrement: true,
});
noteStore.createIndex('title-index', 'title');
}
if (oldVersion < 3) {
// 创建 settings 存储
db.createObjectStore(STORE_NAMES.SETTINGS, {
keyPath: 'key',
});
}
},
blocked: () => {
console.warn('Database is blocked by another tab');
},
terminating: () => {
console.warn('Database connection is terminating');
},
});
return this.db;
} catch (error) {
console.error('Database initialization failed:', error);
throw error;
} finally {
this.isInitializing = false;
}
}
// 获取数据库实例
async getDB() {
if (!this.db) {
return await this.init();
}
return this.db;
}
// 关闭数据库连接
close() {
if (this.db) {
this.db.close();
this.db = null;
}
}
// 检查数据库是否已打开
isOpen() {
return !!this.db;
}
// 健康检查
async healthCheck() {
try {
const db = await this.getDB();
const testKey = await db.add(STORE_NAMES.TODOS, {
text: 'Health check',
completed: false,
createdAt: new Date(),
updatedAt: new Date(),
});
await db.get(STORE_NAMES.TODOS, testKey);
await db.delete(STORE_NAMES.TODOS, testKey);
return { status: 'healthy', message: 'Database is functioning properly' };
} catch (error) {
console.error('Database health check failed:', error);
try {
// 尝试重新初始化
this.close();
await this.init();
return { status: 'degraded', message: 'Database recovered after reinitialization' };
} catch (reinitError) {
return { status: 'unhealthy', message: `Database is unavailable: ${reinitError.message}` };
}
}
}
}
// 创建单例实例
export const dbService = new DatabaseService();
3. 创建自定义 Hook
javascript
// hooks/useIndexedDB.js
import { useState, useEffect, useCallback, useRef } from 'react';
import { dbService, STORE_NAMES } from '../lib/db';
export const useIndexedDB = () => {
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const isMounted = useRef(true);
// 清理函数
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
// 添加项目
const addItem = useCallback(async (storeName, item) => {
try {
const db = await dbService.getDB();
const id = await db.add(storeName, {
...item,
createdAt: new Date(),
updatedAt: new Date(),
});
return id;
} catch (err) {
const error = err instanceof Error ? err : new Error('Unknown error occurred');
if (isMounted.current) {
setError(error);
}
throw error;
}
}, []);
// 获取项目
const getItem = useCallback(async (storeName, key) => {
try {
const db = await dbService.getDB();
return await db.get(storeName, key);
} catch (err) {
const error = err instanceof Error ? err : new Error('Unknown error occurred');
if (isMounted.current) {
setError(error);
}
throw error;
}
}, []);
// 获取所有项目
const getAllItems = useCallback(async (storeName, indexName, query) => {
try {
const db = await dbService.getDB();
if (indexName && query !== undefined) {
const tx = db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
const index = store.index(indexName);
return await index.getAll(query);
}
return await db.getAll(storeName);
} catch (err) {
const error = err instanceof Error ? err : new Error('Unknown error occurred');
if (isMounted.current) {
setError(error);
}
throw error;
}
}, []);
// 更新项目
const updateItem = useCallback(async (storeName, item) => {
try {
const db = await dbService.getDB();
await db.put(storeName, {
...item,
updatedAt: new Date(),
});
} catch (err) {
const error = err instanceof Error ? err : new Error('Unknown error occurred');
if (isMounted.current) {
setError(error);
}
throw error;
}
}, []);
// 删除项目
const deleteItem = useCallback(async (storeName, key) => {
try {
const db = await dbService.getDB();
await db.delete(storeName, key);
} catch (err) {
const error = err instanceof Error ? err : new Error('Unknown error occurred');
if (isMounted.current) {
setError(error);
}
throw error;
}
}, []);
// 清空存储
const clearStore = useCallback(async (storeName) => {
try {
const db = await dbService.getDB();
await db.clear(storeName);
} catch (err) {
const error = err instanceof Error ? err : new Error('Unknown error occurred');
if (isMounted.current) {
setError(error);
}
throw error;
}
}, []);
// 初始化数据库
useEffect(() => {
const initializeDB = async () => {
try {
setIsLoading(true);
await dbService.init();
if (isMounted.current) {
setError(null);
}
} catch (err) {
const error = err instanceof Error ? err : new Error('Failed to initialize database');
if (isMounted.current) {
setError(error);
}
} finally {
if (isMounted.current) {
setIsLoading(false);
}
}
};
initializeDB();
// 清理函数
return () => {
dbService.close();
};
}, []);
return {
isLoading,
error,
addItem,
getItem,
getAllItems,
updateItem,
deleteItem,
clearStore,
};
};
4. 创建针对待办事项的专用 Hook
javascript
// hooks/useTodos.js
import { useCallback } from 'react';
import { useIndexedDB } from './useIndexedDB';
import { STORE_NAMES } from '../lib/db';
export const useTodos = () => {
const {
isLoading,
error,
addItem,
getAllItems,
updateItem,
deleteItem
} = useIndexedDB();
// 获取待办事项
const getTodos = useCallback(async (completed) => {
try {
if (completed !== undefined) {
return await getAllItems(STORE_NAMES.TODOS, 'completed-index', completed);
}
return await getAllItems(STORE_NAMES.TODOS);
} catch (error) {
console.error('Failed to get todos:', error);
throw error;
}
}, [getAllItems]);
// 添加待办事项
const addTodo = useCallback(async (text) => {
if (!text || !text.trim()) {
throw new Error('Todo text cannot be empty');
}
return await addItem(STORE_NAMES.TODOS, {
text: text.trim(),
completed: false,
});
}, [addItem]);
// 切换待办事项状态
const toggleTodo = useCallback(async (id) => {
try {
const todos = await getTodos();
const todo = todos.find(t => t.id === id);
if (!todo) {
throw new Error('Todo not found');
}
await updateItem(STORE_NAMES.TODOS, {
...todo,
completed: !todo.completed,
});
} catch (error) {
console.error('Failed to toggle todo:', error);
throw error;
}
}, [getTodos, updateItem]);
// 删除待办事项
const removeTodo = useCallback(async (id) => {
await deleteItem(STORE_NAMES.TODOS, id);
}, [deleteItem]);
// 更新待办事项文本
const updateTodoText = useCallback(async (id, text) => {
if (!text || !text.trim()) {
throw new Error('Todo text cannot be empty');
}
try {
const todos = await getTodos();
const todo = todos.find(t => t.id === id);
if (!todo) {
throw new Error('Todo not found');
}
await updateItem(STORE_NAMES.TODOS, {
...todo,
text: text.trim(),
});
} catch (error) {
console.error('Failed to update todo text:', error);
throw error;
}
}, [getTodos, updateItem]);
// 批量切换待办事项状态
const toggleAllTodos = useCallback(async (completed) => {
try {
const todos = await getTodos();
const txPromises = todos.map(todo =>
updateItem(STORE_NAMES.TODOS, {
...todo,
completed: completed,
})
);
await Promise.all(txPromises);
} catch (error) {
console.error('Failed to toggle all todos:', error);
throw error;
}
}, [getTodos, updateItem]);
// 清除已完成待办事项
const clearCompleted = useCallback(async () => {
try {
const completedTodos = await getTodos(true);
const deletePromises = completedTodos.map(todo =>
deleteItem(STORE_NAMES.TODOS, todo.id)
);
await Promise.all(deletePromises);
} catch (error) {
console.error('Failed to clear completed todos:', error);
throw error;
}
}, [getTodos, deleteItem]);
return {
isLoading,
error,
getTodos,
addTodo,
toggleTodo,
removeTodo,
updateTodoText,
toggleAllTodos,
clearCompleted,
};
};
5. 在组件中使用
jsx
// components/TodoList.js
import React, { useState, useEffect } from 'react';
import { useTodos } from '../hooks/useTodos';
const TodoList = () => {
const [todos, setTodos] = useState([]);
const [newTodo, setNewTodo] = useState('');
const [filter, setFilter] = useState('all');
const [isAdding, setIsAdding] = useState(false);
const [editingId, setEditingId] = useState(null);
const [editText, setEditText] = useState('');
const { isLoading, error, getTodos, addTodo, toggleTodo, removeTodo, updateTodoText } = useTodos();
// 加载待办事项
useEffect(() => {
const loadTodos = async () => {
if (!isLoading) {
try {
let todosData = [];
switch (filter) {
case 'active':
todosData = await getTodos(false);
break;
case 'completed':
todosData = await getTodos(true);
break;
default:
todosData = await getTodos();
}
setTodos(todosData);
} catch (err) {
console.error('Failed to load todos:', err);
}
}
};
loadTodos();
}, [isLoading, getTodos, filter]);
// 添加新待办事项
const handleAddTodo = async () => {
if (!newTodo.trim() || isAdding) return;
setIsAdding(true);
try {
await addTodo(newTodo);
setNewTodo('');
// 重新加载待办事项
const todosData = await getTodos();
setTodos(todosData);
} catch (err) {
console.error('Failed to add todo:', err);
alert('Failed to add todo: ' + err.message);
} finally {
setIsAdding(false);
}
};
// 切换待办事项状态
const handleToggleTodo = async (id) => {
try {
await toggleTodo(id);
// 重新加载待办事项
const todosData = await getTodos();
setTodos(todosData);
} catch (err) {
console.error('Failed to toggle todo:', err);
alert('Failed to toggle todo: ' + err.message);
}
};
// 删除待办事项
const handleDeleteTodo = async (id) => {
if (!window.confirm('Are you sure you want to delete this todo?')) return;
try {
await removeTodo(id);
// 重新加载待办事项
const todosData = await getTodos();
setTodos(todosData);
} catch (err) {
console.error('Failed to delete todo:', err);
alert('Failed to delete todo: ' + err.message);
}
};
// 开始编辑
const startEditing = (todo) => {
setEditingId(todo.id);
setEditText(todo.text);
};
// 取消编辑
const cancelEditing = () => {
setEditingId(null);
setEditText('');
};
// 保存编辑
const saveEdit = async (id) => {
if (!editText.trim()) {
alert('Todo text cannot be empty');
return;
}
try {
await updateTodoText(id, editText);
setEditingId(null);
setEditText('');
// 重新加载待办事项
const todosData = await getTodos();
setTodos(todosData);
} catch (err) {
console.error('Failed to update todo:', err);
alert('Failed to update todo: ' + err.message);
}
};
if (isLoading) {
return (
<div className="loading">
<div className="spinner"></div>
<p>Loading database...</p>
</div>
);
}
if (error) {
return (
<div className="error">
<h2>Error Loading Todos</h2>
<p>{error.message}</p>
<button onClick={() => window.location.reload()}>Retry</button>
</div>
);
}
const activeCount = todos.filter(t => !t.completed).length;
const completedCount = todos.length - activeCount;
return (
<div className="todo-container">
<h1>Todo List with IndexedDB</h1>
{/* 添加新待办事项 */}
<div className="add-todo">
<input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="What needs to be done?"
onKeyPress={(e) => e.key === 'Enter' && handleAddTodo()}
disabled={isAdding}
/>
<button
onClick={handleAddTodo}
disabled={isAdding || !newTodo.trim()}
className="add-btn"
>
{isAdding ? 'Adding...' : 'Add'}
</button>
</div>
{/* 筛选选项 */}
<div className="filters">
<button
className={filter === 'all' ? 'active' : ''}
onClick={() => setFilter('all')}
>
All ({todos.length})
</button>
<button
className={filter === 'active' ? 'active' : ''}
onClick={() => setFilter('active')}
>
Active ({activeCount})
</button>
<button
className={filter === 'completed' ? 'active' : ''}
onClick={() => setFilter('completed')}
>
Completed ({completedCount})
</button>
</div>
{/* 待办事项列表 */}
{todos.length === 0 ? (
<div className="empty-state">
{filter === 'completed'
? 'No completed todos'
: filter === 'active'
? 'No active todos - great job!'
: 'No todos yet. Add one above!'
}
</div>
) : (
<ul className="todo-list">
{todos.map(todo => (
<li key={todo.id} className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggleTodo(todo.id)}
className="todo-checkbox"
/>
{editingId === todo.id ? (
<div className="edit-mode">
<input
type="text"
value={editText}
onChange={(e) => setEditText(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && saveEdit(todo.id)}
className="edit-input"
autoFocus
/>
<button onClick={() => saveEdit(todo.id)} className="save-btn">Save</button>
<button onClick={cancelEditing} className="cancel-btn">Cancel</button>
</div>
) : (
<div className="view-mode">
<span
className="todo-text"
onDoubleClick={() => startEditing(todo)}
>
{todo.text}
</span>
<div className="todo-actions">
<button
onClick={() => startEditing(todo)}
className="edit-btn"
title="Edit todo"
>
✏️
</button>
<button
onClick={() => handleDeleteTodo(todo.id)}
className="delete-btn"
title="Delete todo"
>
🗑️
</button>
</div>
</div>
)}
</li>
))}
</ul>
)}
{/* 统计信息 */}
<div className="stats">
<small>
Total: {todos.length} |
Active: {activeCount} |
Completed: {completedCount}
</small>
<br />
<small>
Last updated: {todos.length > 0
? new Date(Math.max(...todos.map(t => new Date(t.updatedAt).getTime()))).toLocaleString()
: 'Never'
}
</small>
</div>
</div>
);
};
export default TodoList;
最佳实践和高级用法
1. 重试机制
javascript
// utils/retry.js
export const retryOperation = async (operation, maxRetries = 3, delay = 1000) => {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (error) {
lastError = error;
console.warn(`Operation failed (attempt ${i + 1}/${maxRetries}):`, error);
if (i < maxRetries - 1) {
// 指数退避策略
const waitTime = delay * Math.pow(2, i);
console.log(`Waiting ${waitTime}ms before retry...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
}
}
throw lastError;
};
// 在 Hook 中使用
const getItemWithRetry = async (storeName, key) => {
return retryOperation(() => getItem(storeName, key));
};
2. 批量操作
javascript
// 批量添加项目
const addItemsInBatch = async (storeName, items, batchSize = 100) => {
const db = await dbService.getDB();
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchPromises = batch.map(item =>
store.add({
...item,
createdAt: new Date(),
updatedAt: new Date(),
})
);
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
// 让出主线程,避免阻塞UI
if (i + batchSize < items.length) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
await tx.done;
return results;
};
// 批量删除项目
const deleteItemsInBatch = async (storeName, keys, batchSize = 100) => {
const db = await dbService.getDB();
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
for (let i = 0; i < keys.length; i += batchSize) {
const batch = keys.slice(i, i + batchSize);
const batchPromises = batch.map(key => store.delete(key));
await Promise.all(batchPromises);
// 让出主线程
if (i + batchSize < keys.length) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
await tx.done;
};
3. 数据备份和恢复
javascript
// 导出数据
const exportData = async (storeName) => {
const db = await dbService.getDB();
const allData = await db.getAll(storeName);
const blob = new Blob([JSON.stringify(allData, null, 2)], {
type: 'application/json'
});
return blob;
};
// 导入数据
const importData = async (storeName, jsonData) => {
const data = JSON.parse(jsonData);
const db = await dbService.getDB();
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
// 清空现有数据
await store.clear();
// 添加新数据
for (const item of data) {
await store.add({
...item,
importedAt: new Date(),
});
}
await tx.done;
return data.length;
};
总结
IDB 8.x 提供了强大的 IndexedDB 操作能力,结合 React Hooks 可以创建高效、可靠的客户端数据存储解决方案。本文提供的代码示例展示了:
- 健壮的数据库配置:包含版本迁移、错误处理和单例模式
- 可重用的自定义 Hooks:封装数据库操作逻辑
- 完善的错误处理:包括重试机制和健康检查
- 性能优化:批量操作和事务管理
- 用户体验优化:加载状态、编辑功能和确认对话框
关键最佳实践:
- 使用明确的版本管理策略
- 实现健壮的错误处理和重试机制
- 使用事务确保数据一致性
- 添加适当的加载状态和用户体验优化
- 定期进行数据库健康检查
- 实现数据备份和恢复功能
通过这些实践,你可以在 React 应用中构建出生产级别的客户端数据存储解决方案,提供离线功能和更好的用户体验。