一、IndexedDB介绍
IndexedDB 是一个运行在浏览器上的面向对象数据库。IndexedDB 允许存储和检索用键索引的对象;可以存储结构化克隆算法支持的任何对象。它具有以下几个特点:
- 存储容量大:IndexedDB可以存储GB级别的数据。
- 高效的数据检索:IndexedDB支持索引,可以通过索引进行查询,查询效率高。
- 事务支持:IndexedDB支持事务操作,可以在一个原子操作中执行多个数据库操作,保证数据的一致性。
- 异步操作:使用IndexedDB 执行的操作是异步执行的,以免阻塞应用程序。
- 跨浏览器支持:IndexedDB可以在多个平台和设备上使用,兼容性较好。
- 支持在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常用的增删改查进行了简单的封装,还有其他的一些不常用的方法没有包含在内,如有考虑不周之处,欢迎大家斧正。