在我的项目开发的过程中,为了缩小进步性能,缩小申请,开发者往往须要将一些不易扭转的数据放入本地缓存中。如把用户应用的模板数据放入 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";// 以后用户 idconst 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";// 以后用户 idconst 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 就能够构建 adaptorclass 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";// 以后用户 idconst 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'// }]// 提交到 localStoragestore.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 的回调改为 Promisefunction 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