indexedDB如何使用

89 阅读5分钟

谷歌介绍如下

cd89ffb947850b297c3c320ae22b83e.png

那么我们如何使用呢?

我封装的代码如下

class StorageProxy {
  static ERROR_MESSAGES = {
    DB_INIT_FAILED: 'IndexedDB initialization failed, fallback to localStorage',
    DB_ACCESS_ERROR: 'Error while accessing IndexedDB:',
    STORE_NOT_FOUND: 'Store not found:',
    INVALID_STORE_CONFIG: 'Invalid store configuration'
  };

  constructor(options = {}) {
    this.config = {
      /** 数据库名称 */
      dbName: options.dbName || 'msa',
      /** 数据库版本号 */
      dbVersion: options.dbVersion || 1,
      /** 初始化超时时间(毫秒) */
      initTimeout: options.initTimeout || 5000,
      /** 存储表配置对象 */
      stores: options.stores || {
        default: { keyPath: 'id' }
      }
    };

    this.isIndexedDBSupported = this.checkIndexedDBSupport();
    this.db = null;
    this.initPromise = null;
    this.storeConfigs = new Map();

    if (this.isIndexedDBSupported) {
      this.initPromise = this.initIndexedDB();
    }
  }

  /**  检查是否支持IndexedDB */
  checkIndexedDBSupport() {
    try {
      return !!window.indexedDB;
    } catch {
      return false;
    }
  }

  /**
   *  验证存储配置的基本结构
   * @param {*} config
   * @param {string} config.keyPath - 主键字段名
   * @param {boolean} config.autoIncrement - 是否自增主键
   * @param {Array} config.indexes - 索引配置数组
   * @returns
   */
  validateStoreConfig(config) {
    if (!config || typeof config !== 'object') {
      throw new Error(StorageProxy.ERROR_MESSAGES.INVALID_STORE_CONFIG);
    }
    return {
      keyPath: config.keyPath || 'id',
      indexes: config.indexes || [],
      autoIncrement: !!config.autoIncrement
    };
  }

  /**
   * 异步初始化IndexedDB数据库
   *
   * 此函数通过Promise封装IndexedDB的打开和初始化过程
   * 它首先尝试打开指定名称和版本的数据库如果数据库不存在或版本有变化,
   * 则会触发onupgradeneeded事件,在该事件中可以定义数据库结构的升级逻辑
   * 如果数据库成功打开,将通过onsuccess事件解决Promise,否则通过.onerror事件拒绝Promise
   *
   * @returns {Promise} 返回一个Promise对象,该对象在数据库成功初始化时resolve,在初始化失败时reject
   */
  async initIndexedDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.config.dbName, this.config.dbVersion);

      const timeout = setTimeout(() => {
        reject(new Error('Database initialization timeout'));
      }, this.config.initTimeout);

      request.onupgradeneeded = event => {
        const db = event.target.result;
        this.setupStores(db);
      };

      request.onsuccess = event => {
        clearTimeout(timeout);
        this.db = event.target.result;
        resolve(this.db);
      };

      request.onerror = event => {
        clearTimeout(timeout);
        console.error(StorageProxy.ERROR_MESSAGES.DB_INIT_FAILED, event.target.error);
        this.isIndexedDBSupported = false;
        reject(event.target.error);
      };
    });
  }

  /**
   * 设置数据库中的所有存储
   * 此函数遍历配置中的每个存储,并调用createStore函数来创建它们
   * @param {Object} db - 数据库对象,用于创建存储时参考
   */
  setupStores(db) {
    // 设置所有已配置的存储
    Object.entries(this.config.stores).forEach(([storeName, storeConfig]) => {
      this.createStore(db, storeName, storeConfig);
    });
  }

  /**
   * 在数据库中创建一个新的存储(表)
   * @param {Object} db - 数据库对象
   * @param {string} storeName - 存储的名称
   * @param {Object} storeConfig - 存储的配置对象,包含键路径、是否自动递增和索引配置
   * @throws {Error} 如果存储创建失败,则抛出错误
   */

  createStore(db, storeName, storeConfig) {
    try {
      const config = this.validateStoreConfig(storeConfig);

      // 如果存储已存在,先删除它(数据库版本更新时)
      if (db.objectStoreNames.contains(storeName)) {
        db.deleteObjectStore(storeName);
      }

      // 创建新的存储
      const store = db.createObjectStore(storeName, {
        keyPath: config.keyPath,
        autoIncrement: config.autoIncrement
      });

      // 创建索引
      config.indexes.forEach(indexConfig => {
        store.createIndex(indexConfig.name, indexConfig.keyPath, { unique: !!indexConfig.unique });
      });

      this.storeConfigs.set(storeName, config);
    } catch (error) {
      console.error(`Error creating store ${storeName}:`, error);
      throw error;
    }
  }

  /**
   * 异步方法:添加一个新的存储对象到数据库中
   *
   * 此方法用于在IndexedDB数据库中添加一个新的存储对象配置它包括更新数据库版本、
   * 添加存储对象配置、关闭当前数据库连接并重新初始化数据库
   *
   * @param {string} storeName - 存储对象的名称
   * @param {Object} storeConfig - 存储对象的配置,默认为空对象
   * @returns {Promise} 返回重新初始化数据库的Promise对象
   */
  async addStore(storeName, storeConfig = {}) {
    const currentVersion = this.db?.version || this.config.dbVersion;
    // 更新数据库版本
    this.config.dbVersion = currentVersion + 1;
    this.config.stores[storeName] = storeConfig;

    // 关闭现有连接
    if (this.db) {
      this.db.close();
    }

    // 重新初始化数据库
    return this.initIndexedDB();
  }

  /**
   * 确保数据库连接已建立
   *
   * 此函数用于检查并确保IndexedDB数据库连接已经建立如果浏览器不支持IndexedDB,或者数据库已经初始化,
   * 则直接返回相应的结果如果数据库尚未初始化,将尝试进行初始化如果初始化过程中出现错误,
   * 则标记IndexedDB不被支持,并在控制台记录错误信息
   *
   * @returns {IDBDatabase|null} 返回数据库实例或null
   */
  async ensureDbConnection() {
    // 检查浏览器是否支持IndexedDB,如果不支持则返回null
    if (!this.isIndexedDBSupported) return null;

    // 如果数据库实例已经存在,则直接返回该实例
    if (this.db) return this.db;

    try {
      // 尝试初始化数据库,并返回初始化后的数据库实例
      return await this.initPromise;
    } catch (error) {
      // 如果初始化过程中出现错误,标记IndexedDB不被支持,并记录错误信息
      this.isIndexedDBSupported = false;
      console.error(StorageProxy.ERROR_MESSAGES.DB_ACCESS_ERROR, error);
      return null;
    }
  }

  /**
   * 异步执行IndexedDB操作
   *
   * 此函数用于在指定的存储对象上执行特定的操作(如添加、更新或删除数据)
   * 它首先确保数据库连接已建立,然后检查是否存在指定的存储对象
   * 如果存储对象存在,它将执行给定的操作并返回结果
   * 如果在执行过程中遇到错误,它将抛出错误
   *
   * @param {string} storeName - 要操作的存储对象的名称
   * @param {string} mode - 事务模式,决定了可以执行的操作类型(只读、读写等)
   * @param {function} operation - 在存储对象上执行的操作函数
   * @returns {Promise} 返回一个Promise对象,包含操作的结果或错误
   */
  async executeIndexedDBOperation(storeName, mode, operation) {
    // 确保数据库连接已建立
    const db = await this.ensureDbConnection();
    // 如果数据库连接失败,返回null
    if (!db) return null;

    // 检查是否存在指定的存储对象
    if (!db.objectStoreNames.contains(storeName)) {
      // 如果存储对象不存在,抛出错误
      throw new Error(`${StorageProxy.ERROR_MESSAGES.STORE_NOT_FOUND} ${storeName}`);
    }

    // 返回一个新的Promise对象,用于处理数据库操作
    return new Promise((resolve, reject) => {
      // 开始一个新的事务
      const transaction = db.transaction([storeName], mode);
      // 获取事务中的存储对象
      const store = transaction.objectStore(storeName);

      // 执行给定的操作
      const request = operation(store);

      // 定义成功时的回调函数
      request.onsuccess = event => resolve(event.target.result);
      // 定义错误时的回调函数
      request.onerror = event => {
        // 在控制台中记录错误信息
        console.error(StorageProxy.ERROR_MESSAGES.DB_ACCESS_ERROR, event.target.error);
        // 拒绝Promise,并传递错误对象
        reject(event.target.error);
      };
    });
  }

  /**
   * 异步在指定存储中设置项
   *
   * 此函数首先检查是否支持IndexedDB如果支持,它会根据storeName获取相应的存储配置,
   * 并使用配置中的keyPath或默认的'id'作为键路径,然后将值与键路径信息合并,
   * 并调用executeIndexedDBOperation方法在指定存储中以'readwrite'模式执行put操作,
   * 以保存数据如果IndexedDB不被支持,它将退回到localStorage,
   * 将值以JSON字符串的形式直接存储在localStorage中
   *
   * @param {string} storeName - 存储名称,用于标识要操作的存储空间
   * @param {string|number} key - 要存储的项的键
   * @param {*} value - 要存储的项的值,可以是任意数据类型
   * @returns {Promise} - 返回一个Promise对象,表示异步操作的结果
   */
  async setItem(storeName, key, value) {
    // 检查是否支持IndexedDB
    if (this.isIndexedDBSupported) {
      // 获取指定存储的配置信息
      const storeConfig = this.storeConfigs.get(storeName);
      // 获取键路径,如果没有定义,则使用'id'
      const keyPath = storeConfig?.keyPath || 'id';
      // 将值与键路径信息合并,以适应IndexedDB存储要求
      const data = { ...value, [keyPath]: key };

      // 在指定存储中执行写入操作
      return this.executeIndexedDBOperation(storeName, 'readwrite', store => store.put(data));
    }

    // 如果不支持IndexedDB,使用localStorage进行数据存储
    localStorage.setItem(`${storeName}:${key}`, JSON.stringify(value));
    // 直接返回存储的值
    return value;
  }

  /**
   * 异步获取存储项
   *
   * 此函数尝试从IndexedDB中获取与给定键关联的项如果IndexedDB不被支持,
   * 则退回到localStorage中获取项如果在localStorage中也没有找到该项,则返回null
   *
   * @param {string} storeName - 存储名称,用于IndexedDB中的object store名称,或localStorage中的前缀
   * @param {string} key - 项的键值,用于获取特定的存储项
   * @returns {Promise<any>} - 返回一个Promise,解析为存储项的值如果未找到项,则解析为null
   */
  async getItem(storeName, key) {
    // 检查是否支持IndexedDB
    if (this.isIndexedDBSupported) {
      // 执行IndexedDB操作,使用只读事务获取指定键的值
      const result = await this.executeIndexedDBOperation(storeName, 'readonly', store => store.get(key));
      // 返回结果,如果未找到则为null
      return result || null;
    }

    try {
      // 尝试从localStorage中获取并解析项值
      return JSON.parse(localStorage.getItem(`${storeName}:${key}`));
    } catch {
      // 如果解析失败,返回null
      return null;
    }
  }

  /**
   * 异步移除存储项
   *
   * 此函数旨在从指定的存储中移除一项它首先检查是否支持IndexedDB如果支持,
   * 则执行IndexedDB操作以删除项如果不支持IndexedDB,则回退到使用localStorage
   *
   * @param {string} storeName - 存储名称,表示要在其中执行操作的IndexedDB对象存储或localStorage的前缀
   * @param {string | number} key - 要删除的项的键
   * @returns {Promise<void>} - 表示异步操作完成的Promise对象如果使用localStorage,则没有返回值
   */
  async removeItem(storeName, key) {
    // 检查是否支持IndexedDB
    if (this.isIndexedDBSupported) {
      // 如果支持IndexedDB,则执行删除操作并返回操作完成的Promise
      return this.executeIndexedDBOperation(storeName, 'readwrite', store => store.delete(key));
    }

    // 如果不支持IndexedDB,则使用localStorage移除项
    localStorage.removeItem(`${storeName}:${key}`);
  }

  /**
   * 清空指定存储中的所有数据
   *
   * @param {string} storeName - 存储名称,用于区分不同的数据存储区域
   * @returns {Promise} - 如果使用IndexedDB,则返回一个Promise对象,表示异步操作的结果
   */
  async clear(storeName) {
    // 检查是否支持IndexedDB
    if (this.isIndexedDBSupported) {
      // 如果支持IndexedDB,执行相应的操作
      return this.executeIndexedDBOperation(storeName, 'readwrite', store => store.clear());
    }

    // 如果不支持IndexedDB,只清除指定存储的数据
    const prefix = `${storeName}:`;
    // 遍历localStorage中的所有键
    Object.keys(localStorage).forEach(key => {
      // 如果键以指定前缀开始,表明它属于指定的存储
      if (key.startsWith(prefix)) {
        // 移除属于指定存储的键值对
        localStorage.removeItem(key);
      }
    });
  }

  /**
   * 异步获取存储中的所有项目
   *
   * @param {string} storeName - 存储名称,用于区分不同的数据存储区域
   * @returns {Promise<Array> | Array} - 返回一个Promise或数组,包含存储中的所有项目
   */
  async getAllItems(storeName) {
    // 检查是否支持IndexedDB
    if (this.isIndexedDBSupported) {
      // 如果支持IndexedDB,则执行相应的操作
      return this.executeIndexedDBOperation(storeName, 'readonly', store => store.getAll());
    }

    // 如果不支持IndexedDB,则使用localStorage
    const items = [];
    const prefix = `${storeName}:`;
    // 遍历localStorage中的所有键
    Object.keys(localStorage).forEach(key => {
      // 检查键是否以存储名称为前缀
      if (key.startsWith(prefix)) {
        try {
          // 尝试将存储项解析为JSON对象并添加到项目数组中
          items.push(JSON.parse(localStorage.getItem(key)));
        } catch {}
      }
    });
    // 返回项目数组
    return items;
  }

  /**
   * 异步获取存储在IndexedDB或localStorage中的键集合
   *
   * @param {string} storeName - 存储名称,用于标识存储区域
   * @returns {Promise<Array<string>>} 返回一个Promise对象,解析为键的数组
   */
  async getKeys(storeName) {
    // 检查是否支持IndexedDB
    if (this.isIndexedDBSupported) {
      // 如果支持IndexedDB,执行相应的操作并返回所有键
      return this.executeIndexedDBOperation(storeName, 'readonly', store => store.getAllKeys());
    }

    // 定义localStorage中键的前缀
    const prefix = `${storeName}:`;
    // 从localStorage中获取所有键,过滤出匹配前缀的键,并去除前缀后返回
    return Object.keys(localStorage)
      .filter(key => key.startsWith(prefix))
      .map(key => key.slice(prefix.length));
  }

  /**
   * 使用索引查询
   *
   * 此函数通过指定的索引名称和值,在IndexedDB的某个对象存储中进行查询
   * 它首先检查是否支持IndexedDB,如果不支持则直接返回一个空数组
   * 如果支持,则执行具体的查询操作,并返回查询结果
   *
   * @param {string} storeName 对象存储的名称
   * @param {string} indexName 索引的名称
   * @param {*} value 要查询的索引值
   * @returns {Promise<Array>} 返回一个Promise,解析为查询结果的数组
   */
  async queryByIndex(storeName, indexName, value) {
    // 检查是否支持IndexedDB,如果不支持则返回空数组
    if (!this.isIndexedDBSupported) return [];

    // 执行具体的IndexedDB操作,此处操作类型为只读
    // 通过store参数获取到对象存储,然后根据indexName获取索引
    // 最后通过索引和值进行查询,返回所有匹配的记录
    return this.executeIndexedDBOperation(storeName, 'readonly', store => {
      const index = store.index(indexName);
      return index.getAll(value);
    });
  }
}

使用方式如下

StorageProxy 使用示例文档

1. 初始化

1.1 基础初始化

const storage = new StorageProxy({
  dbName: 'myApp',
  dbVersion: 1,
  stores: {
    users: {
      keyPath: 'id',
      indexes: [
        { name: 'email', keyPath: 'email', unique: true }
      ]
    }
  }
});

1.2 多表初始化

const storage = new StorageProxy({
  dbName: 'myApp',
  dbVersion: 1,
  stores: {
    users: {
      keyPath: 'userId',
      indexes: [
        { name: 'email', keyPath: 'email', unique: true },
        { name: 'username', keyPath: 'username' }
      ]
    },
    orders: {
      keyPath: 'orderId',
      autoIncrement: true,
      indexes: [
        { name: 'userId', keyPath: 'userId' },
        { name: 'status', keyPath: 'status' }
      ]
    },
    products: {
      keyPath: 'productId',
      indexes: [
        { name: 'category', keyPath: 'category' },
        { name: 'price', keyPath: 'price' }
      ]
    }
  }
});

2. 基本操作

2.1 添加数据

// 添加单条数据(指定ID)
await storage.setItem('users', 'user1', {
  id: 'user1',
  name: '张三',
  email: 'zhang@example.com'
});

// 添加数据(自增ID)
await storage.setItem('orders', null, {
  userId: 'user1',
  status: 'pending',
  amount: 100
});

// 批量添加数据示例
const users = [
  { id: 'user1', name: '张三', email: 'zhang@example.com' },
  { id: 'user2', name: '李四', email: 'li@example.com' }
];

for (const user of users) {
  await storage.setItem('users', user.id, user);
}

2.2 查询数据

// 根据ID查询
const user = await storage.getItem('users', 'user1');

// 使用索引查询
const userByEmail = await storage.queryByIndex('users', 'email', 'zhang@example.com');

// 获取所有数据
const allUsers = await storage.getAllItems('users');

// 获取所有键
const allKeys = await storage.getKeys('users');

2.3 更新数据

// 更新整条记录
await storage.setItem('users', 'user1', {
  id: 'user1',
  name: '张三(已更新)',
  email: 'zhang@example.com'
});

// 更新部分字段(需要先查询)
const user = await storage.getItem('users', 'user1');
if (user) {
  await storage.setItem('users', 'user1', {
    ...user,
    name: '新名字'
  });
}

2.4 删除数据

// 删除单条记录
await storage.removeItem('users', 'user1');

// 清空整个表
await storage.clear('users');

3. 高级用法

3.1 动态添加新表

// 动态添加新表
await storage.addStore('comments', {
  keyPath: 'commentId',
  autoIncrement: true,
  indexes: [
    { name: 'articleId', keyPath: 'articleId' },
    { name: 'userId', keyPath: 'userId' }
  ]
});

// 使用新添加的表
await storage.setItem('comments', null, {
  articleId: 'article1',
  userId: 'user1',
  content: '很好的文章!'
});

3.2 事务处理示例

// 模拟下单流程
async function createOrder(userId, products) {
  try {
    // 1. 创建订单
    const order = await storage.setItem('orders', null, {
      userId,
      products,
      status: 'pending',
      createTime: new Date().toISOString()
    });

    // 2. 更新用户订单数量
    const user = await storage.getItem('users', userId);
    await storage.setItem('users', userId, {
      ...user,
      orderCount: (user.orderCount || 0) + 1
    });

    return order;
  } catch (error) {
    console.error('创建订单失败:', error);
    throw error;
  }
}

3.3 分页查询示例

async function getPagedData(storeName, pageSize, pageNumber) {
  try {
    const allItems = await storage.getAllItems(storeName);
    const start = (pageNumber - 1) * pageSize;
    const end = start + pageSize;
    
    return {
      items: allItems.slice(start, end),
      total: allItems.length,
      currentPage: pageNumber,
      totalPages: Math.ceil(allItems.length / pageSize)
    };
  } catch (error) {
    console.error('分页查询失败:', error);
    throw error;
  }
}

3.4 搜索功能示例

async function searchProducts(keyword) {
  try {
    const allProducts = await storage.getAllItems('products');
    return allProducts.filter(product => 
      product.name.toLowerCase().includes(keyword.toLowerCase()) ||
      product.description.toLowerCase().includes(keyword.toLowerCase())
    );
  } catch (error) {
    console.error('搜索失败:', error);
    throw error;
  }
}

4. 实际应用场景

4.1 用户管理系统

class UserManager {
  constructor() {
    this.storage = new StorageProxy({
      dbName: 'userSystem',
      stores: {
        users: {
          keyPath: 'id',
          indexes: [
            { name: 'email', keyPath: 'email', unique: true },
            { name: 'username', keyPath: 'username' }
          ]
        },
        userProfiles: {
          keyPath: 'userId',
          indexes: [
            { name: 'nickname', keyPath: 'nickname' }
          ]
        }
      }
    });
  }

  async createUser(userData) {
    const userId = `user_${Date.now()}`;
    await this.storage.setItem('users', userId, {
      id: userId,
      ...userData,
      createTime: new Date().toISOString()
    });

    await this.storage.setItem('userProfiles', userId, {
      userId,
      nickname: userData.username,
      avatar: null,
      bio: ''
    });

    return userId;
  }

  async updateProfile(userId, profileData) {
    const existingProfile = await this.storage.getItem('userProfiles', userId);
    await this.storage.setItem('userProfiles', userId, {
      ...existingProfile,
      ...profileData
    });
  }

  async getFullUserData(userId) {
    const [user, profile] = await Promise.all([
      this.storage.getItem('users', userId),
      this.storage.getItem('userProfiles', userId)
    ]);

    return { ...user, ...profile };
  }
}

4.2 购物车系统

class ShoppingCart {
  constructor() {
    this.storage = new StorageProxy({
      dbName: 'shopping',
      stores: {
        cart: {
          keyPath: 'id',
          indexes: [
            { name: 'userId', keyPath: 'userId' }
          ]
        }
      }
    });
  }

  async addToCart(userId, product, quantity = 1) {
    const cartItemId = `${userId}_${product.id}`;
    const existingItem = await this.storage.getItem('cart', cartItemId);

    if (existingItem) {
      await this.storage.setItem('cart', cartItemId, {
        ...existingItem,
        quantity: existingItem.quantity + quantity
      });
    } else {
      await this.storage.setItem('cart', cartItemId, {
        id: cartItemId,
        userId,
        productId: product.id,
        quantity,
        price: product.price,
        name: product.name
      });
    }
  }

  async getCartItems(userId) {
    const items = await this.storage.queryByIndex('cart', 'userId', userId);
    return {
      items,
      total: items.reduce((sum, item) => sum + item.price * item.quantity, 0)
    };
  }

  async clearCart(userId) {
    const items = await this.storage.queryByIndex('cart', 'userId', userId);
    for (const item of items) {
      await this.storage.removeItem('cart', item.id);
    }
  }
}

4.3 文章管理系统

class ArticleManager {
  constructor() {
    this.storage = new StorageProxy({
      dbName: 'blog',
      stores: {
        articles: {
          keyPath: 'id',
          indexes: [
            { name: 'author', keyPath: 'author' },
            { name: 'category', keyPath: 'category' },
            { name: 'tags', keyPath: 'tags', multiEntry: true },
            { name: 'publishDate', keyPath: 'publishDate' }
          ]
        }
      }
    });
  }

  async createArticle(articleData) {
    const id = `article_${Date.now()}`;
    await this.storage.setItem('articles', id, {
      id,
      ...articleData,
      createTime: new Date().toISOString(),
      updateTime: new Date().toISOString()
    });
    return id;
  }

  async getByCategory(category) {
    return await this.storage.queryByIndex('articles', 'category', category);
  }

  async getByTag(tag) {
    return await this.storage.queryByIndex('articles', 'tags', tag);
  }

  async getRecentArticles(limit = 10) {
    const all = await this.storage.getAllItems('articles');
    return all
      .sort((a, b) => new Date(b.publishDate) - new Date(a.publishDate))
      .slice(0, limit);
  }
}

这些示例涵盖了 StorageProxy 的大多数使用场景。每个示例都可以根据实际需求进行调整和扩展。

最后欢迎各位大佬点评