4

基于 localStorage 实现有过期时间的存储方式

 1 year ago
source link: https://www.xiabingbao.com/post/fe/local-expired-storage-rqstpj.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

基于 localStorage 实现有过期时间的存储方式

蚊子前端博客
发布于 2023-03-01 00:27
我们介绍下如何基于 localStorage 实现有过期时间的存储方式!

我们知道 localStorage 中的数据是长期保存的,除非手动删除,否则他会一直存在。如果我们想实现一种数据有过期时间的存储方式,该怎么实现呢?

首先应该想到的是 cookie,cookie 本身就有有效期的配置,当某个 cookie 时,浏览器自动清理该 cookie。可是使用 cookie 存储数据,有个不好的地方,很多我们存储的数据,本就是我们前端自己用到的,后端根本用不到。可是存储到 cookie 中后,页面中所有的 cookie 都会随着请求发送给后端,造成传输的 cookie 比较长,而且没有必要。

因此,我们可以基于 localStorage 来实现一套这样的有过期时间的存储方式。我们在之前的文章 如何重写 localStorage 中的方法 中,也了解了一些重写 localStorage 的方法。这里我们是自己在外层封装一层的方式,来调用 localStorage。

我这里封装的类名叫: LocalExpiredStorage,即有过期时间的 localStorage。

1. 实现与 localStorage 基本一致的 api

我们为了实现跟 localStorage 使用上的一致性体验,这里我们自己的 api 名称和实现方式跟 localStorage 基本一致。

interface SetItemOptions {
  maxAge?: number; // 从当前时间往后多长时间过期
  expired?: number; // 过期的准确时间点,优先级比maxAge高
}

class LocalExpiredStorage {
  private prefix = "local-expired-"; // 用于跟没有过期时间的key进行区分

  constructor(prefix?: string) {
    if (prefix) {
      this.prefix = prefix;
    }
  }

  setItem(key: string, value: any, options?: SetItemOptions) {}
  getItem(key: string): any {}
  removeItem(key: string) {}
  clearAllExpired() {}
}
const localExpiredStorage = new LocalExpiredStorage();
export default localExpiredStorage;

可以看到我们实现的类里,有三个变化:

  1. setItem()方法新增了一个 options 参数,这里主要是为了配置过期时间,这里有两种配置方式,一种是可以设置多长时间后过期,比如 2 个小时后过期(开发者不用特殊计算 2 个小时后的时间节点);再一种是设置过期的时间节点,该值可以是格式化的时间,也可以是时间戳;

  2. 有一个 prefix 属性,在具体实现中,我们会将 prefix 属性与操作的 key 进行拼接,标识该 key 是具有过期时间特性的,方便我们自己的类进行处理;

  3. 新增了一个 clearAllExpired() 方法,这是为了清理所有已经过期的 key,避免占用缓存;该方法在应用的入口处就应当调用,便于及时清理;

上面是我们的大致框架,接下来我们来具体实现下这些方法。

2. 具体实现

接下来我们来一一实现这些方法。

2.1 setItem

这里我们新增了一个 options 参数,用来配置过期时间:

  • expired: 固定的过期时间点,比如点击关闭按钮,当前不再展示,那过期时间就是今天晚上的 23:59:59,可以使用该属性;

  • maxAge: 从当前时间起,设置多长时间后过期;比如点击某个提示,3 天内不再展示,使用该属性就比较方便;

假如两个属性都设置了,我这里约定 expired 属性的优先级更高一些。

class LocalExpiredStorage {
  private prefix = "local-expired-"; // 用于跟没有过期时间的key进行区分

  constructor(prefix?: string) {
    if (prefix) {
      this.prefix = prefix;
    }
  }

  setItem(key: string, value: any, options?: SetItemOptions) {
    const now = Date.now();
    let expired = now + 100 * 60 * 60 * 3; // 默认过期时间为3个小时

    // 这里我们限定了 expired 和 maxAge 都是 number 类型,
    // 您也可以扩展支持到 string 类型或者如 { d:2, h:3 } 这种格式
    if (options?.expired) {
      expired = options?.expired;
    } else if (options?.maxAge) {
      expired = now + options.maxAge;
    }

    // 我们这里用了 dayjs 对时间戳进行格式化,方便快速识别
    // 若没这个需要,也可以直接存储时间戳,减少第三方类库的依赖
    localStorage.setItem(
      `${this.prefix}${key}`,
      JSON.stringify({
        value,
        start: dayjs().format("YYYY/MM/DD hh:mm:ss"), // 存储的起始时间
        expired: dayjs(expired).format("YYYY/MM/DD hh:mm:ss"), // 存储的过期时间
      })
    );
  }
}

我们在过期时间的实现过程中,目前只支持了 number 类型,即需要传入一个时间戳,参与运算。您也可以扩展到 string 类型(比如'2024/11/23 14:45:34')或者其他格式{ d:2, h:3 } 这种格式。

设置好过期时间后,我们将 value,存储的起始时间和过期时间,转义成 json string 存储起来。我们这里用了 dayjs 对时间戳进行格式化,方便开发者可以快速地识别。若没有这个需要,也可以直接存储时间戳,减少第三方类库的依赖。

该方法并没有支持永久存储的设定,若您需要永久存储,可以直接使用 localStorage 来存储。

2.2 getItem

获取某 key 存储的值,主要是对过期时间的判断。

class LocalExpiredStorage {
  private prefix = "local-expired-"; // 用于跟没有过期时间的key进行区分

  constructor(prefix?: string) {
    if (prefix) {
      this.prefix = prefix;
    }
  }

  getItem(key: string): any {
    const result = localStorage.getItem(`${this.prefix}${key}`);
    if (!result) {
      // 若key本就不存在,直接返回null
      return result;
    }
    const { value, expired } = JSON.parse(result);
    if (Date.now() <= dayjs(expired).valueOf()) {
      // 还没过期,返回存储的值
      return value;
    }
    // 已过期,删除该key,然后返回null
    this.removeItem(key);
    return null;
  }
  removeItem(key: string) {
    localStorage.removeItem(`${this.prefix}${key}`);
  }
}

在获取 key 时,主要经过 3 个过程:

  1. 若本身就没存储这个 key,直接返回 null;

  2. 已存储了该 key 的数据,解析出数据和过期时间,若还在有效期,则返回存储大数据;

  3. 若已过期,则删除该 key,然后返回 null;

这里我们在删除数据时,使用了this.removeItem(),即自己实现的删除方法。本来我们也是要实现这个方法的,那就直接使用了吧。

2.3 clearAllExpired

localStorage 中的数据并不会自动清理,我们需要一个方法用来手动批量清理已过期的数据。

class LocalExpiredStorage {
  private prefix = "local-expired-"; // 用于跟没有过期时间的key进行区分

  clearAllExpired() {
    let num = 0;

    // 判断 key 是否过期,然后删除
    const delExpiredKey = (key: string, value: string | null) => {
      if (value) {
        // 若value有值,则判断是否过期
        const { expired } = JSON.parse(value);
        if (Date.now() > dayjs(expired).valueOf()) {
          // 已过期
          localStorage.removeItem(key);
          return 1;
        }
      } else {
        // 若 value 无值,则直接删除
        localStorage.removeItem(key);
        return 1;
      }
      return 0;
    };

    const { length } = window.localStorage;
    const now = Date.now();

    for (let i = 0; i < length; i++) {
      const key = window.localStorage.key(i);

      if (key?.startsWith(this.prefix)) {
        // 只处理我们自己的类创建的key
        const value = window.localStorage.getItem(key);
        num += delExpiredKey(key, value);
      }
    }
    return num;
  }
}

在项目的入口处添加上该方法,用户每次进入项目时,都会自动清理一次已过期的 key。

3. 完整的代码

上面我们是分步讲解的,这里我们放下完整的代码。同时,我也在 GitHub 上放了一份:wenzi0github/local-expired-storage

interface SetItemOptions {
  maxAge?: number; // 从当前时间往后多长时间过期
  expired?: number; // 过期的准确时间点,优先级比maxAge高
}

class LocalExpiredStorage {
  private prefix = "local-expired-"; // 用于跟没有过期时间的key进行区分

  constructor(prefix?: string) {
    if (prefix) {
      this.prefix = prefix;
    }
  }

  // 设置数据
  setItem(key: string, value: any, options?: SetItemOptions) {
    const now = Date.now();
    let expired = now + 100 * 60 * 60 * 3; // 默认过期时间为3个小时

    // 这里我们限定了 expired 和 maxAge 都是 number 类型,
    // 您也可以扩展支持到 string 类型或者如 { d:2, h:3 } 这种格式
    if (options?.expired) {
      expired = options?.expired;
    } else if (options?.maxAge) {
      expired = now + options.maxAge;
    }

    // 我们这里用了 dayjs 对时间戳进行格式化,方便快速识别
    // 若没这个需要,也可以直接存储时间戳,减少第三方类库的依赖
    localStorage.setItem(
      `${this.prefix}${key}`,
      JSON.stringify({
        value,
        start: dayjs().format("YYYY/MM/DD hh:mm:ss"), // 存储的起始时间
        expired: dayjs(expired).format("YYYY/MM/DD hh:mm:ss"), // 存储的过期时间
      })
    );
  }

  getItem(key: string): any {
    const result = localStorage.getItem(`${this.prefix}${key}`);
    if (!result) {
      // 若key本就不存在,直接返回null
      return result;
    }
    const { value, expired } = JSON.parse(result);
    if (Date.now() <= dayjs(expired).valueOf()) {
      // 还没过期,返回存储的值
      return value;
    }
    // 已过期,删除该key,然后返回null
    this.removeItem(key);
    return null;
  }

  // 删除key
  removeItem(key: string) {
    localStorage.removeItem(`${this.prefix}${key}`);
  }

  // 清除所有过期的key
  clearAllExpired() {
    let num = 0;

    // 判断 key 是否过期,然后删除
    const delExpiredKey = (key: string, value: string | null) => {
      if (value) {
        // 若value有值,则判断是否过期
        const { expired } = JSON.parse(value);
        if (Date.now() > dayjs(expired).valueOf()) {
          // 已过期
          localStorage.removeItem(key);
          return 1;
        }
      } else {
        // 若 value 无值,则直接删除
        localStorage.removeItem(key);
        return 1;
      }
      return 0;
    };

    const { length } = window.localStorage;
    const now = Date.now();

    for (let i = 0; i < length; i++) {
      const key = window.localStorage.key(i);

      if (key?.startsWith(this.prefix)) {
        // 只处理我们自己的类创建的key
        const value = window.localStorage.getItem(key);
        num += delExpiredKey(key, value);
      }
    }
    return num;
  }
}
const localExpiredStorage = new LocalExpiredStorage();
export default localExpiredStorage;
localExpiredStorage.setItem("key", "value", { maxAge: 5000 }); // 有效期为5000毫秒
localExpiredStorage.setItem("key", "value", {
  expired: Date.now() + 1000 * 60 * 60 * 12,
}); // 有效期为 12 个小时,自己计算到期的时间戳

// 获取数据
localExpiredStorage.getItem("key");

// 删除数据
localExpiredStorage.removeItem("key");

// 清理所有过期的key
localExpiredStorage.clearAllExpired();

这个功能本身不难,也有很多开发者自己实现过。这里我也是总结下之前实现的过程。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK