基于 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;
可以看到我们实现的类里,有三个变化:
- setItem()方法新增了一个 options 参数,这里主要是为了配置过期时间,这里有两种配置方式,一种是可以设置多长时间后过期,比如 2 个小时后过期(开发者不用特殊计算 2 个小时后的时间节点);再一种是设置过期的时间节点,该值可以是格式化的时间,也可以是时间戳;
- 有一个 prefix 属性,在具体实现中,我们会将 prefix 属性与操作的 key 进行拼接,标识该 key 是具有过期时间特性的,方便我们自己的类进行处理;
- 新增了一个 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 个过程:
- 若本身就没存储这个 key,直接返回 null;
- 已存储了该 key 的数据,解析出数据和过期时间,若还在有效期,则返回存储大数据;
- 若已过期,则删除该 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