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

1,018 阅读5分钟

我们知道 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 + 1000 * 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 + 1000 * 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();

4. 总结

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