React Hooks项目中使用IDB 8.x的实现
作者:蓝银草同学
IDB8.x是IndexedDB的轻量级封装库,提供基于Promise的简洁API,结合React Hooks可实现高效、可靠的客户端数据存储,本文就来介绍一下React Hooks项目中使用IDB 8.x的实现,感兴趣的可以了解一下
什么是 IDB?
IDB 是一个轻量级的 IndexedDB 包装库,它提供了基于 Promise 的 API,让 IndexedDB 的使用变得更加简单直观。相比于原生 IndexedDB 复杂的回调机制,IDB 提供了更现代化、更易用的接口。8.x 版本带来了更好的性能和更简洁的 API。
IDB 8.x 核心 API
1. 打开/创建数据库
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 操作
// 添加数据 - 返回生成的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. 事务操作
// 使用事务进行多个操作 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. 高级查询操作
// 使用索引范围查询 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. 安装依赖
npm install idb
2. 创建数据库配置
// 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
// 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
// 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. 在组件中使用
// 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. 重试机制
// 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. 批量操作
// 批量添加项目 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. 数据备份和恢复
// 导出数据 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 应用中构建出生产级别的客户端数据存储解决方案,提供离线功能和更好的用户体验。
到此这篇关于React Hooks项目中使用IDB 8.x的实现的文章就介绍到这了,更多相关React Hooks使用IDB 8.x内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!