注册
web

基于 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-"; // 用于跟没有过期时间的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-"; // 用于跟没有过期时间的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-"; // 用于跟没有过期时间的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-"; // 用于跟没有过期时间的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-"; // 用于跟没有过期时间的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. 总结


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



作者:小蚊酱
来源:juejin.cn/post/7215775714417655867

0 个评论

要回复文章请先登录注册