在我的项目开发的过程中,为了缩小进步性能,缩小申请,开发者往往须要将一些不易扭转的数据放入本地缓存中。如把用户应用的模板数据放入 localStorage 或者 IndexedDB。代码往往如下书写。
// 这里将数据放入内存中
let templatesCache = null;
// 用户 id,用于多账号零碎
const userId: string = '1';
const getTemplates = ({refresh = false} = {refresh: false}) => {
// 不须要立刻刷新,走存储
if (!refresh) {
// 内存中有数据,间接应用内存中数据
if (templatesCache) {return Promise.resolve(templatesCache)
}
const key = `templates.${userId}`
// 从 localStorage 中获取数据
const templateJSONStr = localStroage.getItem(key)
if (templateJSONStr) {
try {templatesCache = JSON.parse(templateJSONStr);
return Promise.resolve(templatesCache)
} catch () {
// 解析失败,革除 storage 中数据
localStroage.removeItem(key)
}
}
}
// 进行服务端掉用获取数据
return api.get('xxx').then(res => {templatesCache = cloneDeep(res)
// 存入 本地缓存
localStroage.setItem(key, JSON.stringify(templatesCache))
return res
})
};
能够看到,代码十分冗余,同时这里的代码还没有解决数据版本、过期工夫以及数据写入等性能。如果再把这些性能点退出,代码将会更加简单,不易保护。
于是集体写了一个小工具 storage-tools 来解决这个问题。
应用 storage-tools 缓存数据
该库默认应用 localStorage 作为数据源,开发者从库中获取 StorageHelper 工具类。
import {StorageHelper} from "storage-tools";
// 以后用户 id
const userId = "1";
// 构建模版 store
// 构建时候就会获取 localStorage 中的数据放入内存
const templatesStore = new StorageHelper({
// 多账号用户应用 key
storageKey: `templates.${userId}`,
// 以后数据版本号,能够从后端获取并传入
version: 1,
// 超时工夫,单位为 秒
timeout: 60 * 60 * 24,
});
// 从内存中获取数据
const templates = templatesStore.getData();
// 没有数据,表明数据过期或者没有存储过
if (templates === null) {api.get("xxx").then((val) => {
// 存储数据到内存中去,之后的 getData 都能够获取到数据
store.setData(val);
// 空闲工夫将以后内存数据存储到 localStorage 中
requestIdleCallback(() => {
// 期间内能够屡次掉用 setData
store.commit();});
});
}
StorageHelper 工具类反对了其余缓存源,代码如下:
import {IndexedDBAdaptor, StorageAdaptor, StorageHelper} from "storage-tools";
// 以后用户 id
const userId = "1";
const sessionStorageStore = new StorageHelper({
// 配置同上
storageKey: `templates.${userId}`,
version: 1,
timeout: 60 * 60 * 24,
// 适配器,传入 sessionStorage
adapter: sessionStorage,
});
const indexedDBStore = new StorageHelper({storageKey: `templates.${userId}`,
version: 1,
timeout: 60 * 60 * 24,
// 适配器,传入 IndexedDBAdaptor
adapter: new IndexedDBAdaptor({
dbName: "userInfo",
storeName: "templates",
}),
});
// IndexedDB 只能异步构建,所以当初只能期待获取构建获取实现
indexedDBStore.whenReady().then(() => {
// 筹备实现后,咱们就能够 getData 和 setData 了
const data = indexedDBStore.getData();
// 其余代码
});
// 只须要有 setItem 和 getItem 就能够构建 adaptor
class MemoryAdaptor implements StorageAdaptor {readonly cache = new Map();
// 获取 map 中数据
getItem(key: string) {return this.cache.get(key);
}
setItem(key: string, value: string) {this.cache.set(key, value);
}
}
const memoryStore = new StorageHelper({
// 配置同上
storageKey: `templates.${userId}`,
version: 1,
timeout: 60 * 60 * 24,
// 适配器,传入携带 getItem 和 setItem 对象
adapter: new MemoryAdaptor(),});
当然了,咱们还能够继承 StorageHelper 构建业务类。
// 也能够基于 StorageHelper 构建业务类
class TemplatesStorage extends StorageHelper {
// 传入 userId 以及 版本
constructor(userId: number, version: number) {
super({storageKey: `templates.${userId}`,
// 如果须要运行时候更新,则能够动静传递
version,
timeout: 60 * 60 * 24,
});
}
// TemplatesStorage 实例
static instance: TemplatesStorage;
// 如果须要版本信息的话,static version: number = 0;
static getStoreInstance() {
// 获取版本信息
return getTemplatesVersion().then((newVersion) => {
// 没有构建实例或者版本信息不相等,间接从新构建
if (newVersion !== TemplatesStorage.version || !TemplatesStorage.instance) {TemplatesStorage.instance = new TemplatesStorage("1", newVersion);
TemplatesStorage.version = newVersion;
}
return TemplatesStorage.instance;
});
}
/**
* 获取模板缓存和 api 申请联合
*/
getTemplates() {const data = super.getData();
if (data) {return Promise.resolve(data);
}
return api.get("xxx").then((val) => {this.setTemplates(val);
return super.getData();});
}
/**
* 保留数据到内存后提交到数据源
*/
setTemplats(templates: any[]) {super.setData(templates);
super.commit();}
}
/**
* 获取模版信息函数
*/
const getTemplates = () => {return TemplatesStorage.getStoreInstance().then((instance) => {return instance.getTemplates();
});
};
针对于某些特定列表程序需要,咱们还能够构建 ListStorageHelper。
import {ListStorageHelper, MemoryAdaptor} from "../src";
// 以后用户 id
const userId = "1";
const store = new ListStorageHelper({storageKey: `templates.${userId}`,
version: 1,
// 设置惟一键 key,默认为 'id'
key: "searchVal",
// 列表存储最大数据量,默认为 10
maxCount: 100,
// 批改数据后是否挪动到最后面,默认为 true
isMoveTopWhenModified: true,
// 增加数据后是否是最后面, 默认为 true
isUnshiftWhenAdded: true,
});
store.setItem({searchVal: "new game"});
store.getData();
// [{
// searchVal: 'new game'
// }]
store.setItem({searchVal: "new game2"});
store.getData();
// 会插入最后面
// [{
// searchVal: 'new game2'
// }, {
// searchVal: 'new game'
// }]
store.setItem({searchVal: "new game"});
store.getData();
// 会更新到最后面
// [{
// searchVal: 'new game'
// }, {
// searchVal: 'new game2'
// }]
// 提交到 localStorage
store.commit();
storage-tools 我的项目演进
任何我的项目都不是一触而就的,上面是对于 storage-tools 库的编写思路。心愿能对大家有一些帮忙。
StorageHelper 反对 localStorage 存储
我的项目的第一步就是反对本地贮存 localStorage 的存取。
// 获取从 1970 年 1 月 1 日 00:00:00 UTC 到用户机器工夫的秒数
// 后续有需要也会向外提供工夫函数配置,能够联合 sync-time 库一起应用
const getCurrentSecond = () => parseInt(`${new Date().getTime() / 1000}`);
// 获取以后空数据
const getEmptyDataStore = (version: number): DataStore<any> => {const currentSecond = getCurrentSecond();
return {
// 以后数据的创立工夫
createdOn: currentSecond,
// 以后数据的批改工夫
modifiedOn: currentSecond,
// 以后数据的版本
version,
// 数据, 空数据为 null
data: null,
};
};
class StorageHelper<T> {
// 存储的 key
private readonly storageKey: string;
// 存储的版本信息
private readonly version: number;
// 内存中数据,不便随时读写
store: DataStore<T> | null = null;
constructor({storageKey, version}) {
this.storageKey = storageKey;
this.version = version || 1;
this.load();}
load() {const result: string | null = localStorage.getItem(this.storageKey);
// 初始化内存信息数据
this.initStore(result);
}
private initStore(storeStr: string | null) {
// localStorage 没有数据,间接构建 空数据放入 store
if (!storeStr) {this.store = getEmptyDataStore(this.version);
return;
}
let store: DataStore<T> | null = null;
try {
// 开始解析 json 字符串
store = JSON.parse(storeStr);
// 没有数据或者 store 没有 data 属性间接构建空数据
if (!store || !("data" in store)) {store = getEmptyDataStore(this.version);
} else if (store.version !== this.version) {
// 版本不统一间接降级
store = this.upgrade(store);
}
} catch (_e) {
// 解析失败了,构建空的数据
store = getEmptyDataStore(this.version);
}
this.store = store || getEmptyDataStore(this.version);
}
setData(data: T) {if (!this.store) {return;}
this.store.data = data;
}
getData(): T | null {if (!this.store) {return null;}
return this.store?.data;
}
commit() {
// 获取内存中的 store
const store = this.store || getEmptyDataStore(this.version);
store.version = this.version;
const now = getCurrentSecond();
if (!store.createdOn) {store.createdOn = now;}
store.modifiedOn = now;
// 存储数据到 localStorage
localStorage.setItem(this.storageKey, JSON.stringify(store));
}
/**
* 获取内存中 store 的信息
* 如 modifiedOn createdOn version 等信息
*/
get(key: DataStoreInfo) {return this.store?.[key];
}
upgrade(store: DataStore<T>): DataStore<T> {
// 获取以后的秒数
const now = getCurrentSecond();
// 看起来很像 getEmptyDataStore 代码,但实际上是不同的业务
// 不应该因为代码类似而合并,不利于前期扩大
return {
// 只获取之前的创立工夫,如果没有应用以后的工夫
createdOn: store?.createdOn || now,
modifiedOn: now,
version: this.version,
data: null,
};
}
}
StorageHelper 增加超时机制
增加超时机制很简略,只须要在 getData 的时候检查一下数据即可。
class StorageHelper<T> {
// 其余代码 ...
// 超时工夫,默认为 -1,即不超时
private readonly timeout: number = -1;
constructor({storageKey, version, timeout}: StorageHelperParams) {
// 传入的数据是数字类型,且大于 0,就设定超时工夫
if (typeof timeout === "number" && timeout > 0) {this.timeout = timeout;}
}
getData(): T | null {if (!this.store) {return null;}
// 如果小于 0 就没有超时工夫,间接返回数据,事实上不可能小于 0
if (this.timeout < 0) {return this.store?.data;}
// 批改工夫加超时工夫大于以后工夫,则示意没有超时
// 留神,每次 commit 都会更新 modifiedOn
if (getCurrentSecond() < (this.store?.modifiedOn || 0) + this.timeout) {return this.store?.data;}
// 版本信息在最开始时候解决过了,此处间接返回 null
return null;
}
}
StorageHelper 增加其余存储适配
此时咱们能够增加其余数据源适配,不便开发者自定义 storage。
/**
* 适配器接口,存在 getItem 以及 setItem
*/
interface StorageAdaptor {getItem: (key: string) => string | Promise<string> | null;
setItem: (key: string, value: string) => void;
}
class StorageHelper<T> {
// 其余代码 ...
// 非浏览器环境不具备 localStorage,所以不在此处间接结构
readonly adapter: StorageAdaptor;
constructor({storageKey, version, adapter, timeout}: StorageHelperParams) {
// 此处没有传递 adapter 就会应用 localStorage
// adapter 对象必须有 getItem 和 setItem
// 此处没有进一步判断 getItem 是否为函数以及 localStorage 是否存在
// 没有方法限制住所有的异样
this.adapter = adapter && "getItem" in adapter && "setItem" in adapter
? adapter
: localStorage;
this.load();}
load() {
// 此处改为 this.adapter
const result: Promise<string> | string | null = this.adapter.getItem(this.storageKey,);
}
commit() {
// 此处改为 this.adapter
this.adapter.setItem(this.storageKey, JSON.stringify(store));
}
}
StorageHelper 增加异步获取
如有些数据源须要异步构建并获取数据,例如 IndexedDB。这里咱们先建设一个 IndexedDBAdaptor 类。
import {StorageAdaptor} from "../utils";
// 把 indexedDB 的回调改为 Promise
function promisifyRequest<T = undefined>(request: IDBRequest<T> | IDBTransaction,): Promise<T> {return new Promise<T>((resolve, reject) => {
// @ts-ignore
request.oncomplete = request.onsuccess = () => resolve(request.result);
// @ts-ignore
request.onabort = request.onerror = () => reject(request.error);
});
}
/**
* 创立并返回 indexedDB 的句柄
*/
const createStore = (
dbName: string,
storeName: string,
upgradeInfo: IndexedDBUpgradeInfo = {},): UseStore => {const request = indexedDB.open(dbName);
/**
* 创立或者降级时候会调用 onupgradeneeded
*/
request.onupgradeneeded = () => {const { result: store} = request;
if (!store.objectStoreNames.contains(storeName)) {const { options = {}, indexList = []} = upgradeInfo;
// 基于 配置项生成 store
const store = request.result.createObjectStore(storeName, { ...options});
// 建设索引
indexList.forEach((index) => {store.createIndex(index.name, index.keyPath, index.options);
});
}
};
const dbp = promisifyRequest(request);
return (txMode, callback) =>
dbp.then((db) =>
callback(db.transaction(storeName, txMode).objectStore(storeName))
);
};
export class IndexedDBAdaptor implements StorageAdaptor {
private readonly store: UseStore;
constructor({dbName, storeName, upgradeInfo}: IndexedDBAdaptorParams) {this.store = createStore(dbName, storeName, upgradeInfo);
}
/**
* 获取数据
*/
getItem(key: string): Promise<string> {return this.store("readonly", (store) => promisifyRequest(store.get(key)));
}
/**
* 设置数据
*/
setItem(key: string, value: string) {return this.store("readwrite", (store) => {store.put(value, key);
return promisifyRequest(store.transaction);
});
}
}
对 StorageHelper 类做如下革新
type CreateDeferredPromise = <TValue>() => CreateDeferredPromiseResult<TValue>;
// 劫持一个 Promise 方便使用
export const createDeferredPromise: CreateDeferredPromise = <T>() => {let resolve!: (value: T | PromiseLike<T>) => void;
let reject!: (reason?: any) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return {
currentPromise: promise,
resolve,
reject,
};
};
export class StorageHelper<T> {
// 是否筹备好了
ready: CreateDeferredPromiseResult<boolean> = createDeferredPromise<
boolean
>();
constructor({storageKey, version, adapter, timeout}: StorageHelperParams) {this.load();
}
load() {
const result: Promise<string> | string | null = this.adapter.getItem(this.storageKey,);
// 检查一下以后的后果是否是 Promise 对象
if (isPromise(result)) {
result
.then((res) => {this.initStore(res);
// 筹备好了
this.ready.resolve(true);
})
.catch(() => {this.initStore(null);
// 筹备好了
this.ready.resolve(true);
});
} else {
// 不是 Promise 间接构建 store
this.initStore(result);
// 筹备好了
this.ready.resolve(true);
}
}
// 询问是否做好筹备
whenReady() {return this.ready.currentPromise;}
}
如此,咱们就实现了 StorageHelper 全副代码。
列表辅助类 ListStorageHelper
ListStorageHelper 基于 StorageHelper 构建,不便特定业务应用。
// 数组最大数量
const STORE_MAX_COUNT: number = 10;
export class ListStorageHelper<T> extends StorageHelper<T[]> {
// 主键,默认为 id
readonly key: string = "id";
// 存储最大数量,默认为 10
readonly maxCount: number = STORE_MAX_COUNT;
// 是否增加在最后面
readonly isUnshiftWhenAdded: boolean = true;
// 批改后是否放入最后面
readonly isMoveTopWhenModified: boolean = true;
constructor({
maxCount,
key,
isMoveTopWhenModified = true,
isUnshiftWhenAdded = true,
storageKey,
version,
adapter,
timeout,
}: ListStorageHelperParams) {super({ storageKey, version, adapter, timeout});
this.key = key || "id";
// 设置配置项
if (typeof maxCount === "number" && maxCount > 0) {this.maxCount = maxCount;}
if (typeof isMoveTopWhenModified === "boolean") {this.isMoveTopWhenModified = isMoveTopWhenModified;}
if (typeof this.isUnshiftWhenAdded === "boolean") {this.isUnshiftWhenAdded = isUnshiftWhenAdded;}
}
load() {super.load();
// 没有数据,设定为空数组不便对立
if (!this.store!.data) {this.store!.data = [];
}
}
getData = (): T[] => {const items = super.getData() || [];
// 检查数据长度并移除超过的数据
this.checkThenRemoveItem(items);
return items;
};
setItem(item: T) {if (!this.store) {throw new Error("Please complete the loading load first");
}
const items = this.getData();
// 利用 key 去查找存在数据索引
const index = items.findIndex((x: any) => x[this.key] === (item as any)[this.key],
);
// 以后有数据,是更新
if (index > -1) {const current = { ...items[index], ...item };
// 更新挪动数组数据
if (this.isMoveTopWhenModified) {items.splice(index, 1);
items.unshift(current);
} else {items[index] = current;
}
} else {
// 增加
this.isUnshiftWhenAdded ? items.unshift(item) : items.push(item);
}
// 查看并移除数据
this.checkThenRemoveItem(items);
}
removeItem(key: string | number) {if (!this.store) {throw new Error("Please complete the loading load first");
}
const items = this.getData();
const index = items.findIndex((x: any) => x[this.key] === key);
// 移除数据
if (index > -1) {items.splice(index, 1);
}
}
setItems(items: T[]) {if (!this.store) {return;}
this.checkThenRemoveItem(items);
// 批量设置数据
this.store.data = items || [];}
/**
* 多增加一个办法 getItems,等同于 getData 办法
*/
getItems() {if (!this.store) {return null;}
return this.getData();}
checkThenRemoveItem = (items: T[]) => {if (items.length <= this.maxCount) {return;}
items.splice(this.maxCount, items.length - this.maxCount);
};
}
该类继承了 StorageHelper,咱们仍旧能够间接调用 commit 提交数据。如此咱们就不须要保护简单的 storage 存取逻辑了。
代码都在 storage-tools 中,欢送各位提交 issue 以及 pr。
激励一下
如果你感觉这篇文章不错,心愿能够给与我一些激励,在我的 github 博客下帮忙 star 一下。
博客地址
参考资料
storage-tools
sync-time