DAO 层在前端的应用

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情

前言

关于 DAO(Data access object)层一直以来是后端讨论的话题,其实 DAO 层的概念也可以应用于前端

什么是 DAO

DAO:抽象和封装对数据源的访问与存储,DAO 通过对数据源链接的管理方便对数据的访问与存储

大量的处理数据,在前端并不常见,这往往是早后端需要做的事情,封装 DAO 层连接数据库,简化访问数据显得格外必要

而在前端,我们经常要与本地数据库交互,浏览器提供的原生 API 并不能很好的满足我们的需求,因此我们得对原生 API 进行二次封装

对于开发者来说,不必了解 DAO 层的实现,只需要使用 DAO 提供的简单的 API 就可以进行数据处理

localstorage 遇到的问题

本文以 localstorage 举例,以此来说明 DAO 在前端的应用

当我们使用 localstorage 设置数据的时候总会遇到些问题:

  • 新增的数据会不会覆盖之前的数据
  • 怎么给数据设置过期时间,获取超时的数据会不会有对应的提示
  • 多人使用本地数据库,怎么避免重名的问题
  • 。。。

对于这些问题不应该在每一次与本地数据库交互都要做出额外的判断,这些逻辑具有极大的相似性,完全可以内聚在 DAO 层里

开始

处理数据不外乎 CRUD,所以我们就从这里入手

怎么避免变量重名

解决办法:每个人在创建变量的时候添加不同的标识符

因为在多人协作的时候,每个人都会交互数据库,如果是使用相同的 DAO,可能会引起冲突,所以,每个人都应该使用 DAO 的实例,因此,我们需要将 DAO 封装成为

class DAO {
  private preId: string;

  constructor(preId: string = '') {
    if (!DAO.preIds.some(item => item === preId)) {
      this.preId = preId;
      DAO.preIds.push(preId);
    } else {
      throw new Error('存在重复的 preId')
    }
  }

  static preIds: string[] = []
}
复制代码

我们在类型添加了一个静态属性存储已经实例过的 preId,如果传入的 preId 重复,会提示对应的信息,以 preId_数据 作为 key 存在本地数据库里

变量可以按需添加过期时间

在某些时候我们可以按需给变量设置一个过期时间,对于设置了过期时间的变量我们得让时间与数据分离开 数据-|-过期时间 ,将这个作为 value,存储在本地数据库里

class DAO {
  private preId: string;
  private timeSign: string;

  static preIds: string[] = []

  constructor(preId: string = '', timeSign = '-|-') {
    if (!DAO.preIds.some(item => item === preId)) {
      this.preId = preId;
      this.timeSign = timeSign;
      DAO.preIds.push(preId);
    } else {
      throw new Error('存在重复的 preId')
    }
  }

  set() { }

  get() { }

  remove() { }
}
复制代码

对于时间和数据分隔符,我们提供了自定义的选择,用户可以自己设置分隔符,同时我们还在 DAO 的原型上定义了三个方法,用来封装处理数据的方法

处理数据的状态

对于处理数据的结果可能因为 localstorage 的内存大小,或者因为数据设置了过期时间从而导致操作的异常,并且应该提示对应的状态

class DAO {
  private preId: string;
  private timeSign: string;

  static preIds: string[] = [];

  static storage: Storage = localStorage || window.localStorage; // 将 localstorage 作为静态属性存储,方便使用

  static status = {
    SUCCESS: 'success',
    FAILURE: 'failure',
    OVERFLOW: 'overflow',
    TIMEOUT: 'timeout'
  }

  constructor(preId: string = '', timeSign = '-|-') {
    if (!DAO.preIds.some(item => item === preId)) {
      this.preId = preId;
      this.timeSign = timeSign;
      DAO.preIds.push(preId);
    } else {
      throw new Error('存在重复的 preId')
    }
  }

  set() { }

  get() { }

  remove() { }
}
复制代码

在此我们搭建好了 DAO 层的架子

处理数据

Create 和 Update

为了更好的操作键值对我们在 DAO 的原型的上面添加了 生成 keyvalue 的方法

  private setKey(key: string): string {
    return this.preId + key;
  }

  private setValue(value: string, startTime: string): string {
    return startTime ? (startTime + this.timeSign + value) : value;
  }
复制代码
  set(
    key: string,
    value: string,
    cb?: (status: TStatus, key: string, value: string) => void,  // 执行完时候的回调函数
    time: string | Date = '', // 是否设置起始时间
    validTime: number = 0, // 设置过期时间
  ) {
    let status: TStatus = DAO.status.SUCCESS;
    let startTime: string = '';

    if (time) {
      startTime = (new Date(time).getTime() || new Date(new Date()).getTime()) + '';
      validTime = validTime || 7 * 24 * 60 * 60; // 默认设置七天过期时间
    }

    this.startTime = startTime;
    this.validTime = validTime;
    DAO.storage.setItem(this.setKey(key), this.setValue(value, startTime));

    cb && cb.apply(this, [status, key, value]);

    return <IResult>{
      status,
      key,
      value,
    }
  }
复制代码

Remove

在删除和获取 localstorage 的数据的时候,首先要根据 key,判断是否存在这个 key,然后进行对应的操作

  private findValue(
    key: string,
    successFn: (status: TStatus, value: string) => { status: TStatus, value: string }, // value 值存在的时候执行的回到函数,
    failFn: () => TStatus,
    cb?: (status: TStatus, key: string, value: string | null) => void,
  ): IResult {
    let status: TStatus = DAO.status.SUCCESS;
    let value: string | null = DAO.storage.getItem(this.setKey(key));

    if (!value) {
      // status = this.status.FAILURE;
      status = failFn();
    } else {
      // value 存在
      if (successFn) {
        const successRes = successFn(status, value);
        status = successRes.status;
        value = successRes.value;
      }
    }
    cb && cb.apply(this, [status, key, value]);

    return {
      status,
      key,
      value,
    }
  }
复制代码

在这个方法中,value 是否存在的 if-else 分支的里面执行对应的回调

因为我们对每个 localstroage 设置了特殊的标志,所以在获取值的时候也要处理

  private getValue(value: string) {
    let index = value.indexOf(this.startTime ? this.timeSign : '');
    value = this.startTime ? value.slice(index + this.timeSign.length) : value;

    return value;
  }
复制代码
  remove(
    key: string,
    cb?: (status: TStatus, key: string, value: string | null) => void
  ): IResult {
    return this.findValue(
      key,
      // 成功的回调
      (status, value) => {
        value = this.getValue(value);
        DAO.storage.removeItem(this.setKey(key));

        return {
          status,
          value
        }
      },
      // 失败的回调
      () => {
        return DAO.status.FAILURE;
      },

      cb
    )
  }
复制代码

Read

  get(
    key: string,
    cb?: (status: TStatus, key: string, value: string | null) => void
  ): IResult {
    return this.findValue(key,
      // 成功的回调
      (status, value) => {
        // 判断是否超时
        if ((this.startTime && new Date().getTime() < parseInt(this.startTime) + this.validTime) || !this.startTime) {
          //  没超时
          value = this.getValue(value);
        } else {
          // 超时
          status = DAO.status.TIMEOUT;
          value = null as unknown as string; // 将超时 value 转为 null
          this.remove(key);
        }

        return {
          status,
          value,
        }
      },
      // 失败的回调
      () => {
        return DAO.status.FAILURE;
      },

      cb
    )
  }
复制代码

使用

我们封装好了 DAO,现在我们测试一下

不设置过期时间

function test(...args: any): any {
  console.log(args);
}

const dao = new DAO('test__', '--|--');

dao.set('name', 'xiaoming', test)
dao.get('name', test)
dao.remove('name', test)
复制代码

image.png

设置数据成功的时候,本地数据库: image.png

设置过期时间

const dao2 = new DAO('tes2__');

dao2.set('name', 'xiaoming', test, '2022-08-31') // 在七天之内到期
dao2.get('name', test)
dao2.remove('name', test)
复制代码

image.png

设置数据成功的时候,本地数据库: image.png

总结

本文通过二次封装 localstorage,向你展示 DAO 层在前端的应用,本文还有有细节处没实现,譬如,超出localstorage 内存大小,产生对应的提示...

你还可以利用 DAO 的思想封装 sessionStoragecookie

DAO 是一个对象,通过封装方法和属性管理数据库,对于使用者来说,不必了解 DAO 层的内部实现,只需要使用对应的 DAO 提供出的方法就可以操作数据库

分类:
前端