在 React Hooks 项目中使用 IDB 8.x 的完整指南

什么是 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 可以创建高效、可靠的客户端数据存储解决方案。本文提供的代码示例展示了:

  1. 健壮的数据库配置:包含版本迁移、错误处理和单例模式
  2. 可重用的自定义 Hooks:封装数据库操作逻辑
  3. 完善的错误处理:包括重试机制和健康检查
  4. 性能优化:批量操作和事务管理
  5. 用户体验优化:加载状态、编辑功能和确认对话框

关键最佳实践:

  • 使用明确的版本管理策略
  • 实现健壮的错误处理和重试机制
  • 使用事务确保数据一致性
  • 添加适当的加载状态和用户体验优化
  • 定期进行数据库健康检查
  • 实现数据备份和恢复功能

通过这些实践,你可以在 React 应用中构建出生产级别的客户端数据存储解决方案,提供离线功能和更好的用户体验。

相关推荐
whysqwhw6 小时前
KNOI Gradle Plugin 模块分析
前端
墨染 殇雪6 小时前
web安全-XSS注入
前端·web安全·xss·跨站脚本
我家猫叫佩奇6 小时前
👾 Life of a Pixel
前端·javascript
LilyCoder6 小时前
HTML5国庆网站源码
前端·html·html5
龙在天6 小时前
vue前端开发,如何做虚拟列表?要不卡顿,要体验感好
前端
龙在天7 小时前
虚拟列表 如何 计算页面能 显示多少列表项 ?
前端
前端的日常7 小时前
使用Trae生成交互式地铁线路图
前端·trae
头孢头孢7 小时前
API Key 认证 + 滑动窗口限流:保护接口安全的实战方案
前端·javascript·bootstrap
木西7 小时前
React Native DApp 开发全栈实战·从 0 到 1 系列(NFT交易-前端部分)
前端·react native