前端indexedDB工具封装

102 阅读5分钟

一、IndexedDB介绍

IndexedDB 是一个运行在浏览器上的面向对象数据库。IndexedDB 允许存储和检索用索引的对象;可以存储结构化克隆算法支持的任何对象。它具有以下几个特点:

  1. 存储容量大:IndexedDB可以存储GB级别的数据。
  2. 高效的数据检索:IndexedDB支持索引,可以通过索引进行查询,查询效率高。
  3. 事务支持:IndexedDB支持事务操作,可以在一个原子操作中执行多个数据库操作,保证数据的一致性。
  4. 异步操作:使用IndexedDB 执行的操作是异步执行的,以免阻塞应用程序。
  5. 跨浏览器支持:IndexedDB可以在多个平台和设备上使用,兼容性较好。
  6. 支持在worker环境下调用。

二、indexedDB的封装

虽然浏览器提供了很多indexedDB的API,但是在大型项目中管理indexedDB的连接,对表进行维护以及对数据进行操作时不免有些繁琐,因此笔者对indexedDB进行了结构化的封装,简化了操作indexDB的逻辑,使得调用indexedDB更加方便,封装代码如下:

1.web版

IndexDBUtils.ts工具导出

// IndexDBUtils.ts


//,创建,删除表,都会更新version ,创建删除的总计操作不要超过1000次
const maxReInitializationCount = 1000;

class IndexedDBSingleton {
  // 内部维护的静态实例  
  private static instance: IndexedDBSingleton;
  // 内部的连接实例
  private db: IDBDatabase | null = null;
  // 连接的数据库名
  private readonly dbName: string = 'projectIndexDB';
  // 初始化版本号,创建表会升级,连接时会把这个版本号和数据库实际版本号对齐
  private version: number = 1;
  // 创建的配置信息
  private storeConfigs: StoreConfig[] = [];

  private batchMax: number = 100; // 批量操作的最大数量

  constructor(dbName: string = 'classPlatWebDB', batchMax: number = 100) {
    if (!dbName || dbName.trim().length === 0) {
      throw new Error('db name error');
    }

    if (IndexedDBSingleton.instance) {
      return IndexedDBSingleton.instance;
    }

    this.batchMax = batchMax;
    this.dbName = dbName;
    IndexedDBSingleton.instance = this;
  }

  /**
   * 检查表是否存在
   * @param tableName
   */
  public isExistTable = (tableName: string) => {
    if (this.db) {
      return this.db.objectStoreNames.contains(tableName);
    }
    return false;
  };

  public isExistDB() {
    return this.db !== null;
  }

  /**
   * 初始化数据库连接,同步本地版本和数据库实际版本
   */
  public async initialize() {
    if (this.db) return Promise.resolve(true);

    let reInitialization = false;

    let currentTryTimes = 0;

    const initFn = () => {
      return new Promise((resolve, _reject) => {
        const request = indexedDB.open(this.dbName, this.version);

        request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
          const db = (event.target as IDBOpenDBRequest).result;
          this.handleUpgrade(db, event.oldVersion);
        };

        request.onsuccess = (event: Event) => {
          this.db = (event.target as IDBOpenDBRequest).result;

          resolve(true);
        };

        request.onerror = (_event: Event) => {
          resolve(false);
        };
      });
    };
    // 同步本地版本号和实际版本号
    while (currentTryTimes < maxReInitializationCount && !reInitialization) {
      const initRes = await initFn();
      currentTryTimes++;
      if (initRes) {
        reInitialization = true;
        return Promise.resolve(true);
      } else {
        this.version++;
      }
    }
  }

  /**
   * 处理数据库升级
   */
  private handleUpgrade(db: IDBDatabase, _oldVersion: number) {
    // 删除不再需要的表
    Array.from(db.objectStoreNames).forEach((storeName) => {
      if (!this.storeConfigs.some((c) => c.name === storeName)) {
        db.deleteObjectStore(storeName);
      }
    });

    // 创建新表
    this.storeConfigs.forEach((config) => {
      if (!db.objectStoreNames.contains(config.name)) {
        this.createStore(db, config);
      }
    });
  }

  /**
   * 创建对象存储
   */
  private createStore(db: IDBDatabase, config: StoreConfig) {
    const store = db.createObjectStore(config.name, config.options);

    config.indexes?.forEach((index) => {
      store.createIndex(index.name, index.keyPath, index.options);
    });
  }

  /**
   * 添加新的表
   */
  public async addStore(config: StoreConfig): Promise<void> {
    if (this.storeConfigs.some((c) => c.name === config.name)) {
      throw new Error(`Store ${config.name} already exists`);
    }

    this.storeConfigs.push(config);
    this.version++;
    await this.reconnect();
  }

  /**
   * 删除数据库表
   */
  public async deleteStore(storeName: string): Promise<void> {
    this.storeConfigs = this.storeConfigs.filter((c) => c.name !== storeName);
    this.version++;
    await this.reconnect();
  }

  /**
   * 重新连接数据库
   */
  private async reconnect(): Promise<void> {
    if (this.db) {
      this.db.close();
      this.db = null;
    }
    await this.initialize();
  }

  /**
   * 添加数据,如果主键已存在则抛出错误
   * @throws {Error}主键已存在
   * @param storeName
   * @param data
   * @returns Promise<IDBValidKey>返回主键key
   */
  public async add<T>(storeName: string, data: T): Promise<IDBValidKey> {
    return this.execute(storeName, 'readwrite', (store) => {
      // store.getAllKeys()
      return store.add(data);
    });
  }

  /**
   * 获取指定表存储的所有key
   * @param storeName
   */
  public async getAllKeys(storeName: string) {
    return this.execute(storeName, 'readwrite', (store) => {
      return store.getAllKeys();
    });
  }

  /**
   * 根据主键获取数据
   * @param storeName
   * @param key
   */
  public async get<T>(
    storeName: string,
    key: IDBValidKey,
  ): Promise<T | undefined> {
    return this.execute(storeName, 'readonly', (store) => {
      return store.get(key);
    });
  }

  /**
   * 添加数据,如果主键已存在则更新
   * @param storeName
   * @param data
   * @returns Promise<IDBValidKey>返回主键key
   */
  public async put<T>(storeName: string, data: T): Promise<IDBValidKey> {
    return this.execute(storeName, 'readwrite', (store) => {
      return store.put(data);
    });
  }

  /**
   * 删除某个表的数据
   * @param storeName
   * @param key
   */
  public async delete(storeName: string, key: IDBValidKey): Promise<void> {
    return this.execute(storeName, 'readwrite', (store) => {
      return store.delete(key);
    });
  }

  /**
   * 清除指定表的所有数据
   * @param storeName
   */
  public async clear(storeName: string): Promise<void> {
    return this.execute(storeName, 'readwrite', (store) => {
      return store.clear();
    });
  }

  /**
   * 批量删除数据
   * @param storeName 存储对象名称
   * @param keys 主键列表
   * @param batchSize 分批大小(默认100)
   */
  public async deleteAll(
    storeName: string,
    keys: Array<IDBValidKey> = [],
    batchSize = this.batchMax,
  ) {
    let total: number = 0;
    for (let i = 0; i < keys.length; i += batchSize) {
      const batch = keys.slice(i, i + batchSize);
      const count: number = await this.executeBatch<IDBValidKey>(
        storeName,
        'readwrite',
        (store, key) => store.delete(key),
        batch,
      );
      total += count;
    }
    return total;
  }

  /**
   * 批量添加数据
   * @param storeName
   * @param items
   * @param batchSize
   */
  public async addAll<T>(
    storeName: string,
    items: Array<T> = [],
    batchSize = this.batchMax,
  ) {
    let total: number = 0;
    for (let i = 0; i < items.length; i += batchSize) {
      const batch = items.slice(i, i + batchSize);
      const count: number = await this.executeBatch<T>(
        storeName,
        'readwrite',
        (store, item) => store.add(item),
        batch,
      );
      total += count;
    }
    return total;
  }

  /**
   * 批量添加[如果主键存在,更新]数据
   * @param storeName
   * @param items
   * @param batchSize
   */
  public async putAll<T>(
    storeName: string,
    items: Array<T> = [],
    batchSize = this.batchMax,
  ) {
    let total: number = 0;
    for (let i = 0; i < items.length; i += batchSize) {
      const batch = items.slice(i, i + batchSize);
      const count: number = await this.executeBatch<T>(
        storeName,
        'readwrite',
        (store, item) => store.put(item),
        batch,
      );
      total += count;
    }
    return total;
  }

  /**
   * 通用事务执行方法
   * @param storeName
   * @param mode
   * @param operation
   * @returns {Promise<T>}
   */
  private async execute<T>(
    storeName: string,
    mode: IDBTransactionMode,
    operation: (store: IDBObjectStore) => IDBRequest<T>,
  ): Promise<T> {
    if (!this.db) throw new Error('Database not initialized');

    return new Promise((resolve, reject) => {
      const transaction = this.db!.transaction(storeName, mode);
      const store = transaction.objectStore(storeName);

      const request = operation(store);

      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);

      transaction.oncomplete = () => {};
      transaction.onerror = () => reject(transaction.error);
    });
  }

  /**
   * 通用批量事务执行方法
   * @param storeName
   * @param mode
   * @param operation
   * @param batch
   * @returns {Promise<unknown>}
   */
  private async executeBatch<T>(
    storeName: string,
    mode: IDBTransactionMode,
    operation: (store: IDBObjectStore, item: T) => IDBRequest,
    batch: Array<T>,
  ): Promise<number> {
    if (!this.db) {
      throw new Error('Database not initialized');
    }
    return new Promise((resolve, reject) => {
      let successCount = 0;
      let errorOccurred = false;

      const transaction = this.db!.transaction(storeName, mode);
      const store = transaction.objectStore(storeName);

      batch &&
        batch.forEach((item) => {
          if (errorOccurred) return Promise.reject(successCount);

          let request: IDBRequest;
          try {
            // 执行具体操作(put/add/delete等)
            request = operation(store, item);
          } catch (error) {
            errorOccurred = true;
            transaction.abort();
            reject(error);
            return;
          }
          request.onsuccess = () => {
            successCount++;
          };

          request.onerror = (event) => {
            errorOccurred = true;
            transaction.abort();
            // console.log()
            reject(event.target);
          };
        });

      transaction.oncomplete = () => {
        if (!errorOccurred) resolve(successCount);
      };
      transaction.onerror = () => reject(transaction.error);
    });
  }
}

// 类型定义
interface StoreConfig {
  name: string;
  options: IDBObjectStoreParameters;
  indexes?: IndexConfig[];
}

interface IndexConfig {
  name: string;
  keyPath: string | string[];
  options: IDBIndexParameters;
}

// 创建并导出单例实例
const dbInstance = new IndexedDBSingleton();
// initialize,内部包含更新版本号,是异步的,必须要await


// 这里保留了两种初始化方式的代码,
// 第一种初始化方式直接在当前工具暴露实例之前初始化
// 第二种是在第一次使用时初始化
// 两种都是单例模式,仅仅是初始化时机不同
// 笔者更推荐使用第二种初始化方式。
/**
// 初始化方法1
  await dbInstance.initialize();
  export default dbInstance;

**/
// 初始化方式2
export const getDBInstance = async (): Promise<IndexedDBSingletonType> => {
  if (dbInstance.isExistDB()) {
    return dbInstance;
  }
  await dbInstance.initialize();
  return dbInstance;
};

// 判断浏览器是否支持indexedDB
export function isSupportIndexDB() {
  return !!indexedDB;
}

export type IndexedDBSingletonType = typeof dbInstance;

utils使用

// userCacheDB.ts
const STORE_NAME = 'userCache';

/**
 * 初始化数据库表
 */
const getUserInstance = async (): Promise<IndexedDBSingletonType> => {
  const instance = await getDBInstance();

  if (!instance.isExistTable(STORE_NAME)) {
    //不存在表,则创建表后返回
    await instance.addStore({
      name: STORE_NAME,
      // STORE创建参数
      options: { keyPath: 'userId' },
      // 索引配置
      indexes: [
        {
          name: 'userIdIndex',
          keyPath: 'userId',
          options: { unique: true },
        },
      ],
    });
    return instance;
  } else {
    return instance;
  }
};





const userInstance = await getUserInstance();

userInstance.get<T>(...options);
userInstance.put<T>;

2.worker版

IndexDB.worker.js

//IndexDB.worker.js
"use strict";
(function () {
    let maxReInitialLimitCount = 1000;

    class IndexedDBSingleton {
        static #instance = null;
        #version = 1;
        #db = null;
        #storeConfigs = [];
        #dbName = "";
        #batchMax = 100;
        constructor({
                        dbName,
                        batchMax = 100,
                        maxInitialLimitCount = 1000
                    }) {
            if (!dbName || dbName.trim().length === 0) {
                throw new Error("db name error");
            }
            if (maxReInitialLimitCount && maxInitialLimitCount > maxReInitialLimitCount) {
                maxReInitialLimitCount = maxInitialLimitCount;
            }
            if (IndexedDBSingleton.#instance) {
                return IndexedDBSingleton.#instance;
            }
            this.#dbName = dbName;
            this.#batchMax = batchMax;
            IndexedDBSingleton.#instance = this;
        }

        isExistDB = () => {
            return this.#db !== null;
        }

        isExistTable = (tableName) => {
            if (this.#db) {
                return this.#db.objectStoreNames.contains(tableName);
            }
            return false;
        }

        // 初始化
        async initialize() {
            if (!self.indexedDB) {
                throw new Error("current environment not supported IndexDB");
            }
            if (this.#db) return Promise.resolve(true);
            let reInitialed = false;

            let currentTryTimes = 0;
            const initFn = () => {
                return new Promise((resolve, _reject) => {
                    const request = self.indexedDB.open(this.#dbName, this.#version);
                    request.onupgradeneeded = (event) => {
                        const db = event.target.result;
                        this.#handleUpgrade(db, event.oldVersion);
                    };

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

                    request.onerror = (_event) => {
                        resolve(false);
                    };
                })
            }

            while (currentTryTimes < maxReInitialLimitCount && !reInitialed) {
                const initRes = await initFn();
                currentTryTimes++;
                if (initRes) {
                    reInitialed = true;
                    return Promise.resolve(true);
                } else {
                    this.#version++;
                }
            }
            return Promise.reject("创建失败");
        }

        /**
         * 销毁数据库
         */
        destroy() {
            if (this.#db) {
                this.#db.close();
                this.#db = null;
            }
        }

        #handleUpgrade(db, _oldVersion) {
            // 删除不再需要的表
            Array.from(db.objectStoreNames).forEach((storeName) => {
                if (!this.#storeConfigs.some((c) => c.name === storeName)) {
                    db.deleteObjectStore(storeName);
                }
            });

            // 创建新表
            this.#storeConfigs.forEach((config) => {
                if (!db.objectStoreNames.contains(config.name)) {
                    this.#createStore(db, config);
                }
            });
        }

        /**
         * 创建对象存储
         */
        #createStore(db, config) {
            const store = db.createObjectStore(config.name, config.options);
            config.indexes?.forEach((index) => {
                store.createIndex(index.name, index.keyPath, index.options);
            });
        }


        /**
         * 添加新的表
         */
        async addStore(config) {
            if (this.#storeConfigs.some((c) => c.name === config.name)) {
                throw new Error(`Store ${config.name} already exists`);
            }

            this.#storeConfigs.push(config);
            this.#version++;
            await this.#reconnect();
        }

        /**
         * 删除数据库表
         */
        async deleteStore(storeName) {
            this.#storeConfigs = this.#storeConfigs.filter((c) => c.name !== storeName);
            this.#version++;
            await this.#reconnect();
        }

        /**
         * 重新连接数据库
         */
        async #reconnect() {
            this.destroy();
            await this.initialize();
        }


        /**
         * 通用事务执行方法
         */
        async #execute(
            storeName,
            mode,
            operation
        ) {
            if (!this.#db) throw new Error("Database not initialized");

            return new Promise((resolve, reject) => {
                const transaction = this.#db.transaction(storeName, mode);
                const store = transaction.objectStore(storeName);

                const request = operation(store);

                request.onsuccess = () => resolve(request.result);
                request.onerror = () => reject(request.error);

                transaction.oncomplete = () => {
                };
                transaction.onerror = () => reject(transaction.error);
            });
        }

        /**
         * 通用批量事务执行方法
         * @param storeName
         * @param mode
         * @param operation
         * @param batch
         * @returns {Promise<unknown>}
         */
        async #executeBatch(storeName,
                            mode,
                            operation, batch) {
            if (!this.#db) {
                throw new Error("Database not initialized");
            }
            return new Promise((resolve, reject) => {
                let successCount = 0;
                let errorOccurred = false;

                const transaction = this.#db.transaction(storeName, mode);
                const store = transaction.objectStore(storeName);

                batch && batch.forEach((item) => {
                    if (errorOccurred) return;

                    let request;
                    try {
                        // 执行具体操作(put/add/delete等)
                        request = operation(store, item);
                    } catch (error) {
                        errorOccurred = true;
                        transaction.abort();
                        reject(error);
                        return;
                    }
                    request.onsuccess = () => {
                        successCount++;
                    };

                    request.onerror = (event) => {
                        errorOccurred = true;
                        transaction.abort();
                        reject(event.target.error);
                    };

                })

                transaction.oncomplete = () => {
                    if (!errorOccurred) resolve(successCount);
                };
                transaction.onerror = () => reject(transaction.error);
            });

        }

        /**
         * 添加数据,如果主键已存在则抛出错误
         * @throws {Error}主键已存在
         * @param storeName
         * @param data
         * @returns 主键key
         */
        async add(storeName, data) {
            return this.#execute(storeName, "readwrite", (store) => {
                return store.add(data);
            });
        }

        /**
         * 批量添加数据(自动生成主键)
         * @param storeName 存储对象名称
         * @param dataList 数据列表
         * @param batchSize 分批大小(默认100)
         */
        async addAll(storeName, dataList, batchSize = this.#batchMax) {
            let total = 0;
            for (let i = 0; i < dataList.length; i += batchSize) {
                const batch = dataList.slice(i, i + batchSize);
                const count = await this.#executeBatch(
                    storeName,
                    "readwrite",
                    (store, item) => store.add(item),
                    batch
                );
                total += count;
            }
            return total;
        }

        /**
         * 根据主键获取数据
         * @param storeName
         * @param key
         */
        async get(storeName, key) {
            return this.#execute(storeName, "readonly", (store) => {
                return store.get(key);
            });
        }

        /**
         * 获取所有数据
         * @param storeName 表名
         * @param query 主键key或比较条件
         * @param count 数量
         */
        async getAll(storeName, query, count) {
            return this.#execute(storeName, "readonly", (store) => {
                return store.getAll(query, count);
            });
        }


        /**
         * @example db.getAllByIndexName("storeName", "indexName", "key", 10);
         * @example db.getAllByIndexName("storeName", "indexName", IDBKeyRange.only("1212"));
         * @param storeName 表名
         * @param indexName 索引名
         * @param query 主键key或比较条件
         * @param count 数量
         */
        async getAllByIndexName(storeName, indexName, query, count) {
            return this.#execute(storeName, "readonly", (store) => {
                const index = store.index(indexName);
                return index.getAll(query, count);
            });
        }

        /**
         * @example db.getCount("storeName");
         * 获取数据的数量
         * @param storeName
         */
        async getCount(storeName) {
            return this.#execute(storeName, "readonly", (store) => {
                return store.count();
            });
        }

        /**
         * @example db.put("storeName", { id: 1, name: "test" });
         * 添加数据,如果主键已存在则更新
         * @param storeName
         * @param data
         * @returns Promise<IDBValidKey> 返回主键key
         */
        async put(storeName, data) {
            return this.#execute(storeName, "readwrite", (store) => {
                return store.put(data);
            });
        }


        /**
         * 批量添加
         * @param storeName
         * @param dataList
         * @param batchSize
         * @returns {Promise<number>}
         */
        async putAll(storeName, dataList, batchSize = this.#batchMax) {
            let total = 0;
            for (let i = 0; i < dataList.length; i += batchSize) {
                const batch = dataList.slice(i, i + batchSize);
                const count = await this.#executeBatch(
                    storeName,
                    "readwrite",
                    (store, item) => store.put(item),
                    batch
                );
                total += count;
            }
            return total;

        }

        /**
         * @example db.delete("storeName", { id: 1, name: "test" });
         * 删除数据
         * @param storeName
         * @param key
         */
        async delete(storeName, key) {
            return this.#execute(storeName, "readwrite", (store) => {
                return store.delete(key);
            });
        }

        /**
         * 批量删除数据
         * @param storeName 存储对象名称
         * @param keys 主键列表
         * @param batchSize 分批大小(默认100)
         */
        async deleteAll(storeName, keys = [], batchSize = this.#batchMax) {
            let total = 0;
            for (let i = 0; i < keys.length; i += batchSize) {
                const batch = keys.slice(i, i + batchSize);
                const count = await this.#executeBatch(
                    storeName,
                    "readwrite",
                    (store, key) => store.delete(key),
                    batch
                );
                total += count;
            }
            return total;
        }

        /**
         * @example db.clear("storeName");
         * 清除所有数据
         * @param storeName
         */
        async clear(storeName) {
            return this.#execute(storeName, "readwrite", (store) => {
                return store.clear();
            });
        }
    }

    self.SingletonDBInstance = IndexedDBSingleton;
})();

worker中使用
"use strict";
importScripts('./IndexDB.worker.js');

// 用闭包防止污染
(function () {
    // 数据库是否初始化
    let isInitDB = false;

    // 数据库实例,new的时候还未初始化,第一次收到任务才会创建连接
    const dbInstance = new SingletonDBInstance({
        dbName: "userTestDB"
    });



    /**
     * 1. 初始化数据库和表
     * @returns {Promise<void>}
     */
    async function initDBAndTable() {
        try {
            // 创建/连接数据库
            if (!dbInstance.isExistDB()) {
                await dbInstance.initialize();
            }

            // 创建表
            if (!dbInstance.isExistTable("userTestDB")) {
                console.log("有表吗")
                await dbInstance.addStore({
                    name: "userTestDB",
                    options: {
                        keyPath: "userId",
                    },
                    indexes: [
                        {
                            name: "userIdIndex",
                            keyPath: "userId",
                            options: {
                                unique: true
                            }
                        },
                    ]
                })

            }
            isInitDB = true;

        } catch (e) {
            self.postMessage({
                type: 'db-error',
                message: "初始化数据库表失败"
            })
            isInitDB = false;
        }
    }
    
    // 在onMessage中调用initDBAndTable方法

})()

笔者仅仅是对indexedDB常用的增删改查进行了简单的封装,还有其他的一些不常用的方法没有包含在内,如有考虑不周之处,欢迎大家斧正。