React Hooks項目中使用IDB 8.x的實現(xiàn)
什么是 IDB?
IDB 是一個輕量級的 IndexedDB 包裝庫,它提供了基于 Promise 的 API,讓 IndexedDB 的使用變得更加簡單直觀。相比于原生 IndexedDB 復雜的回調機制,IDB 提供了更現(xiàn)代化、更易用的接口。8.x 版本帶來了更好的性能和更簡潔的 API。
IDB 8.x 核心 API
1. 打開/創(chuàng)建數(shù)據庫
import { openDB } from 'idb';
// 打開或創(chuàng)建數(shù)據庫 - 推薦使用版本管理
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');
}
},
// 數(shù)據庫被其他標簽頁阻塞時的處理
blocked(currentVersion, blockedVersion) {
console.warn(`Database is blocked by version ${blockedVersion}`);
// 可以提示用戶關閉其他標簽頁
},
// 數(shù)據庫連接終止時的處理
terminating() {
console.warn('Database connection is terminating');
},
// 數(shù)據庫關閉時的處理
closed() {
console.log('Database connection closed');
}
});
};
// 使用示例
const db = await openDatabase();
2. 基本 CRUD 操作
// 添加數(shù)據 - 返回生成的ID
const addData = async () => {
const id = await db.add('store1', {
name: 'Alice',
age: 30,
createdAt: new Date()
});
return id;
};
// 讀取數(shù)據
const getData = async (id) => {
const data = await db.get('store1', id);
return data;
};
// 更新數(shù)據 - put 方法會創(chuàng)建或更新
const updateData = async (id, updates) => {
const existing = await db.get('store1', id);
await db.put('store1', {
...existing,
...updates,
updatedAt: new Date()
});
};
// 刪除數(shù)據
const deleteData = async (id) => {
await db.delete('store1', id);
};
// 計數(shù) - 獲取存儲中的對象數(shù)量
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. 創(chuàng)建數(shù)據庫配置
// lib/db.js
import { openDB } from 'idb';
// 數(shù)據庫常量
export const DB_NAME = 'myAppDB';
export const DB_VERSION = 3;
export const STORE_NAMES = {
TODOS: 'todos',
NOTES: 'notes',
SETTINGS: 'settings'
};
// 數(shù)據庫服務類
class DatabaseService {
constructor() {
this.db = null;
this.isInitializing = false;
}
// 初始化數(shù)據庫
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) {
// 創(chuàng)建 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) {
// 創(chuàng)建 notes 存儲
const noteStore = db.createObjectStore(STORE_NAMES.NOTES, {
keyPath: 'id',
autoIncrement: true,
});
noteStore.createIndex('title-index', 'title');
}
if (oldVersion < 3) {
// 創(chuàng)建 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;
}
}
// 獲取數(shù)據庫實例
async getDB() {
if (!this.db) {
return await this.init();
}
return this.db;
}
// 關閉數(shù)據庫連接
close() {
if (this.db) {
this.db.close();
this.db = null;
}
}
// 檢查數(shù)據庫是否已打開
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}` };
}
}
}
}
// 創(chuàng)建單例實例
export const dbService = new DatabaseService();
3. 創(chuàng)建自定義 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);
// 清理函數(shù)
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;
}
}, []);
// 初始化數(shù)據庫
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();
// 清理函數(shù)
return () => {
dbService.close();
};
}, []);
return {
isLoading,
error,
addItem,
getItem,
getAllItems,
updateItem,
deleteItem,
clearStore,
};
};
4. 創(chuàng)建針對待辦事項的專用 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]);
// 切換待辦事項狀態(tài)
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]);
// 批量切換待辦事項狀態(tài)
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);
}
};
// 切換待辦事項狀態(tài)
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>
)}
{/* 統(tǒng)計信息 */}
<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) {
// 指數(shù)退避策略
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. 數(shù)據備份和恢復
// 導出數(shù)據
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;
};
// 導入數(shù)據
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);
// 清空現(xiàn)有數(shù)據
await store.clear();
// 添加新數(shù)據
for (const item of data) {
await store.add({
...item,
importedAt: new Date(),
});
}
await tx.done;
return data.length;
};
總結
IDB 8.x 提供了強大的 IndexedDB 操作能力,結合 React Hooks 可以創(chuàng)建高效、可靠的客戶端數(shù)據存儲解決方案。本文提供的代碼示例展示了:
- 健壯的數(shù)據庫配置:包含版本遷移、錯誤處理和單例模式
- 可重用的自定義 Hooks:封裝數(shù)據庫操作邏輯
- 完善的錯誤處理:包括重試機制和健康檢查
- 性能優(yōu)化:批量操作和事務管理
- 用戶體驗優(yōu)化:加載狀態(tài)、編輯功能和確認對話框
關鍵最佳實踐:
- 使用明確的版本管理策略
- 實現(xiàn)健壯的錯誤處理和重試機制
- 使用事務確保數(shù)據一致性
- 添加適當?shù)募虞d狀態(tài)和用戶體驗優(yōu)化
- 定期進行數(shù)據庫健康檢查
- 實現(xiàn)數(shù)據備份和恢復功能
通過這些實踐,你可以在 React 應用中構建出生產級別的客戶端數(shù)據存儲解決方案,提供離線功能和更好的用戶體驗。
到此這篇關于React Hooks項目中使用IDB 8.x的實現(xiàn)的文章就介紹到這了,更多相關React Hooks使用IDB 8.x內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
- 使用React Hooks模擬類組件的生命周期方法
- react中函數(shù)式組件React Hooks詳解
- React使用Hooks從服務端獲取數(shù)據的完整指南
- React中不適當?shù)腍ooks使用問題及解決方案
- React Hooks中模擬Vue生命周期函數(shù)的指南
- React hooks如何清除定時器并驗證效果
- react hooks實現(xiàn)防抖節(jié)流的方法小結
- React?hooks中useState踩坑之異步的問題
- React hooks異步操作踩坑記錄
- react?hooks頁面實時刷新方式(setInterval)
- React?Hooks的useState、useRef使用小結
相關文章
React-Native之TextInput組件的設置以及如何獲取輸入框的內容
這篇文章主要介紹了React-Native之TextInput組件的設置以及如何獲取輸入框的內容問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-05-05
react中通過props實現(xiàn)父子組件間通信的使用示例
在React中,父組件可以通過props屬性向子組件傳遞數(shù)據,子組件可以通過props屬性接收父組件傳遞過來的數(shù)據,本文就來介紹一下如何實現(xiàn),感興趣的可以了解一下2023-10-10

