256 lines
7.9 KiB
JavaScript
256 lines
7.9 KiB
JavaScript
![]() |
// uni-storage-helper.js
|
|||
|
class UniStorageHelper {
|
|||
|
constructor(dbName, options = {}) {
|
|||
|
this.dbName = dbName;
|
|||
|
this.storesMeta = {};
|
|||
|
this.options = {
|
|||
|
maxEntries: 500, // 单个存储空间最大条目数
|
|||
|
maxSizeMB: 1, // 单条数据最大限制(微信小程序限制)
|
|||
|
autoPurge: true, // 是否自动清理旧数据
|
|||
|
purgeBatch: 10, // 自动清理批次数量
|
|||
|
debug: false, // 调试模式
|
|||
|
...options
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
/*==================
|
|||
|
核心方法
|
|||
|
==================*/
|
|||
|
|
|||
|
/**
|
|||
|
* 初始化存储空间
|
|||
|
* @param {Array} stores - 存储空间配置
|
|||
|
*/
|
|||
|
async openDB(stores = []) {
|
|||
|
stores.forEach(store => {
|
|||
|
const storeKey = this._getStoreKey(store.name);
|
|||
|
if (!this._storageHas(storeKey)) {
|
|||
|
this._storageSet(storeKey, []);
|
|||
|
}
|
|||
|
|
|||
|
this.storesMeta[store.name] = {
|
|||
|
keyPath: store.keyPath,
|
|||
|
autoIncrement: !!store.autoIncrement,
|
|||
|
indexes: store.indexes || []
|
|||
|
};
|
|||
|
|
|||
|
if (store.autoIncrement) {
|
|||
|
const counterKey = this._getCounterKey(store.name);
|
|||
|
if (!this._storageHas(counterKey)) {
|
|||
|
this._storageSet(counterKey, 1);
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
this._log('数据库初始化完成');
|
|||
|
return Promise.resolve();
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 添加数据(自动处理容量限制)
|
|||
|
*/
|
|||
|
async add(storeName, data) {
|
|||
|
try {
|
|||
|
const storeKey = this._getStoreKey(storeName);
|
|||
|
let storeData = this._storageGet(storeKey) || [];
|
|||
|
const meta = this.storesMeta[storeName];
|
|||
|
const items = Array.isArray(data) ? data : [data];
|
|||
|
|
|||
|
// 容量预检
|
|||
|
await this._checkCapacity(storeName, items);
|
|||
|
|
|||
|
// 处理自增ID
|
|||
|
if (meta?.autoIncrement) {
|
|||
|
const counterKey = this._getCounterKey(storeName);
|
|||
|
let nextId = this._storageGet(counterKey) || 1;
|
|||
|
items.forEach(item => {
|
|||
|
item[meta.keyPath] = nextId++;
|
|||
|
this._createIndexes(meta.indexes, item);
|
|||
|
});
|
|||
|
this._storageSet(counterKey, nextId);
|
|||
|
}
|
|||
|
|
|||
|
// 保存数据
|
|||
|
storeData = [...storeData, ...items];
|
|||
|
this._storageSet(storeKey, storeData);
|
|||
|
|
|||
|
this._log(`成功添加${items.length}条数据到${storeName}`);
|
|||
|
|
|||
|
return meta?.autoIncrement ?
|
|||
|
Array.isArray(data) ?
|
|||
|
items.map(i => i[meta.keyPath]) :
|
|||
|
items[0][meta.keyPath] :
|
|||
|
undefined;
|
|||
|
|
|||
|
} catch (error) {
|
|||
|
if (error.message.includes('exceed')) {
|
|||
|
this._log('触发自动清理...');
|
|||
|
await this._purgeData(storeName, this.options.purgeBatch);
|
|||
|
return this.add(storeName, data);
|
|||
|
}
|
|||
|
throw error;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/*==================
|
|||
|
查询方法
|
|||
|
==================*/
|
|||
|
|
|||
|
async get(storeName, key) {
|
|||
|
const storeData = this._storageGet(this._getStoreKey(storeName)) || [];
|
|||
|
const keyPath = this.storesMeta[storeName]?.keyPath;
|
|||
|
return storeData.find(item => item[keyPath] === key);
|
|||
|
}
|
|||
|
|
|||
|
async getAll(storeName) {
|
|||
|
return this._storageGet(this._getStoreKey(storeName)) || [];
|
|||
|
}
|
|||
|
|
|||
|
async queryByField(storeName, fieldName, value) {
|
|||
|
const storeData = this._storageGet(this._getStoreKey(storeName)) || [];
|
|||
|
return storeData.filter(item => item[fieldName] === value);
|
|||
|
}
|
|||
|
|
|||
|
/*==================
|
|||
|
更新/删除方法
|
|||
|
==================*/
|
|||
|
|
|||
|
async update(storeName, data, key) {
|
|||
|
const storeKey = this._getStoreKey(storeName);
|
|||
|
const storeData = this._storageGet(storeKey) || [];
|
|||
|
const meta = this.storesMeta[storeName];
|
|||
|
const keyPath = meta?.keyPath;
|
|||
|
const targetKey = key ?? data[keyPath];
|
|||
|
|
|||
|
const index = storeData.findIndex(item => item[keyPath] === targetKey);
|
|||
|
if (index === -1) throw new Error('未找到对应记录');
|
|||
|
|
|||
|
// 合并数据并重建索引
|
|||
|
const newItem = {
|
|||
|
...storeData[index],
|
|||
|
...data
|
|||
|
};
|
|||
|
this._createIndexes(meta.indexes, newItem);
|
|||
|
|
|||
|
storeData[index] = newItem;
|
|||
|
this._storageSet(storeKey, storeData);
|
|||
|
|
|||
|
return "更新成功";
|
|||
|
}
|
|||
|
|
|||
|
async delete(storeName, key) {
|
|||
|
const storeKey = this._getStoreKey(storeName);
|
|||
|
const storeData = this._storageGet(storeKey) || [];
|
|||
|
const keyPath = this.storesMeta[storeName]?.keyPath;
|
|||
|
const newData = storeData.filter(item => item[keyPath] !== key);
|
|||
|
this._storageSet(storeKey, newData);
|
|||
|
return `删除${storeData.length - newData.length}条记录`;
|
|||
|
}
|
|||
|
|
|||
|
/*==================
|
|||
|
存储管理
|
|||
|
==================*/
|
|||
|
|
|||
|
async clearStore(storeName) {
|
|||
|
this._storageSet(this._getStoreKey(storeName), []);
|
|||
|
return "存储空间已清空";
|
|||
|
}
|
|||
|
|
|||
|
async deleteDB() {
|
|||
|
Object.keys(this.storesMeta).forEach(storeName => {
|
|||
|
uni.removeStorageSync(this._getStoreKey(storeName));
|
|||
|
uni.removeStorageSync(this._getCounterKey(storeName));
|
|||
|
});
|
|||
|
return "数据库已删除";
|
|||
|
}
|
|||
|
|
|||
|
/*==================
|
|||
|
私有方法
|
|||
|
==================*/
|
|||
|
|
|||
|
_getStoreKey(storeName) {
|
|||
|
return `${this.dbName}_${storeName}`;
|
|||
|
}
|
|||
|
|
|||
|
_getCounterKey(storeName) {
|
|||
|
return `${this.dbName}_${storeName}_counter`;
|
|||
|
}
|
|||
|
|
|||
|
_createIndexes(indexes, item) {
|
|||
|
indexes.forEach(index => {
|
|||
|
item[index.name] = item[index.key];
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
async _checkCapacity(storeName, newItems) {
|
|||
|
const storeKey = this._getStoreKey(storeName);
|
|||
|
const currentData = this._storageGet(storeKey) || [];
|
|||
|
|
|||
|
// 检查条目数限制
|
|||
|
if (currentData.length + newItems.length > this.options.maxEntries) {
|
|||
|
await this._purgeData(storeName, newItems.length);
|
|||
|
}
|
|||
|
|
|||
|
// 检查单条数据大小
|
|||
|
newItems.forEach(item => {
|
|||
|
const sizeMB = this._getItemSizeMB(item);
|
|||
|
if (sizeMB > this.options.maxSizeMB) {
|
|||
|
throw new Error(`单条数据大小超出${this.options.maxSizeMB}MB限制`);
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
_getItemSizeMB(item) {
|
|||
|
try {
|
|||
|
// 精确计算(支持Blob的环境)
|
|||
|
return new Blob([JSON.stringify(item)]).size / 1024 / 1024;
|
|||
|
} catch {
|
|||
|
// 兼容方案
|
|||
|
return encodeURIComponent(JSON.stringify(item)).length * 2 / 1024 / 1024;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
async _purgeData(storeName, count) {
|
|||
|
const storeKey = this._getStoreKey(storeName);
|
|||
|
const currentData = this._storageGet(storeKey) || [];
|
|||
|
const newData = currentData.slice(count);
|
|||
|
this._storageSet(storeKey, newData);
|
|||
|
this._log(`自动清理${count}条旧数据`);
|
|||
|
}
|
|||
|
|
|||
|
/*==================
|
|||
|
存储适配器
|
|||
|
==================*/
|
|||
|
|
|||
|
_storageHas(key) {
|
|||
|
return !!uni.getStorageSync(key);
|
|||
|
}
|
|||
|
|
|||
|
_storageGet(key) {
|
|||
|
try {
|
|||
|
return uni.getStorageSync(key);
|
|||
|
} catch (e) {
|
|||
|
return null;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
_storageSet(key, value) {
|
|||
|
try {
|
|||
|
uni.setStorageSync(key, value);
|
|||
|
return true;
|
|||
|
} catch (error) {
|
|||
|
if (error.errMsg?.includes('exceed')) {
|
|||
|
throw new Error('STORAGE_QUOTA_EXCEEDED');
|
|||
|
}
|
|||
|
throw error;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
_log(...args) {
|
|||
|
if (this.options.debug) {
|
|||
|
console.log(`[StorageHelper]`, ...args);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
export default UniStorageHelper;
|