使用localForage實(shí)現(xiàn)帶過期時(shí)間的本地存儲(chǔ)方案
前言
在前端開發(fā)中,我們經(jīng)常需要將數(shù)據(jù)存儲(chǔ)在客戶端,以減少網(wǎng)絡(luò)請(qǐng)求次數(shù),提高用戶體驗(yàn)。localStorage 和 sessionStorage 是常用的存儲(chǔ)方案,但它們有一些局限性,例如同步 API、只能存儲(chǔ)字符串以及大小限制等。為了解決這些問題,本文將介紹如何使用 localForage 實(shí)現(xiàn)一個(gè)帶過期時(shí)間的本地存儲(chǔ)方案。
什么是 localForage?
localForage 是一個(gè)優(yōu)雅的本地存儲(chǔ)庫(kù),提供了異步的 API,支持存儲(chǔ)多種類型的數(shù)據(jù)(如對(duì)象、數(shù)組、二進(jìn)制數(shù)據(jù)等),并且在內(nèi)部?jī)?yōu)先使用 IndexedDB,如果不可用則回退到 WebSQL 或 localStorage。
需求分析
我們希望實(shí)現(xiàn)以下功能:
- 數(shù)據(jù)存儲(chǔ):能夠存儲(chǔ)任意類型的數(shù)據(jù)。
- 過期時(shí)間:支持設(shè)置數(shù)據(jù)的過期時(shí)間,過期后自動(dòng)清除。
- 持久化:數(shù)據(jù)在刷新或重新打開頁面后仍然存在,直到過期時(shí)間到達(dá)。
實(shí)現(xiàn)思路
為了實(shí)現(xiàn)帶過期時(shí)間的本地存儲(chǔ),我們需要在存儲(chǔ)數(shù)據(jù)的同時(shí),記錄其過期時(shí)間。在讀取數(shù)據(jù)時(shí),先檢查是否過期,若未過期則返回?cái)?shù)據(jù),否則刪除數(shù)據(jù)并返回 null。
代碼實(shí)現(xiàn)
下面是具體的代碼實(shí)現(xiàn):
import localforage from 'localforage';
// 配置 localForage
localforage.config({
name: '存儲(chǔ)名稱,請(qǐng)根據(jù)項(xiàng)目名稱和需求來命名',
});
// 定義一個(gè)永不過期的標(biāo)志
const NEVER_EXPIRES_FLAG = -1;
/**
* 設(shè)置存儲(chǔ)項(xiàng)
* @param k 鍵名
* @param v 值
* @param expired 過期時(shí)間(分鐘),默認(rèn)永不過期
* @returns Promise
*/
export const setItem = (k: string, v: any, expired: number = -1) => {
const expiredKey = `${k}__expires__`;
let exp = 0;
if (expired === NEVER_EXPIRES_FLAG) {
exp = NEVER_EXPIRES_FLAG;
} else if (expired >= 0) {
exp = Date.now() + 1000 * 60 * expired;
}
// 存儲(chǔ)過期時(shí)間
localforage.setItem(expiredKey, exp.toString()).catch((err) => {
console.error('設(shè)置過期時(shí)間失敗:', err);
});
// 存儲(chǔ)實(shí)際數(shù)據(jù)
return localforage.setItem(k, v);
};
/**
* 獲取存儲(chǔ)項(xiàng)
* @param k 鍵名
* @returns Promise<any | null>
*/
export const getItem = async (k: string) => {
const expiredKey = `${k}__expires__`;
try {
const expiredValue = await localforage.getItem<string | null>(expiredKey);
if (expiredValue === null) {
// 未設(shè)置過期時(shí)間,視為不存在
return null;
}
const expiredTime = parseInt(expiredValue, 10);
if (expiredTime === NEVER_EXPIRES_FLAG) {
// 永不過期
return localforage.getItem(k);
}
if (expiredTime > Date.now()) {
// 未過期,返回?cái)?shù)據(jù)
return localforage.getItem(k);
} else {
// 已過期,刪除數(shù)據(jù)
removeItem(k);
removeItem(expiredKey);
return null;
}
} catch (err) {
console.error('獲取數(shù)據(jù)失敗:', err);
return null;
}
};
/**
* 刪除存儲(chǔ)項(xiàng)
* @param k 鍵名
* @returns Promise
*/
export const removeItem = (k: string) => {
const expiredKey = `${k}__expires__`;
localforage.removeItem(expiredKey).catch((err) => {
console.error('刪除過期時(shí)間失敗:', err);
});
return localforage.removeItem(k);
};
代碼解析
配置 localForage
localforage.config({
name: 'bdsg-client',
});
- name:為應(yīng)用程序指定一個(gè)名稱,便于在瀏覽器中區(qū)分存儲(chǔ)。
定義永不過期的標(biāo)志
const NEVER_EXPIRES_FLAG = -1;
- 使用
-1作為永不過期的標(biāo)志。
設(shè)置存儲(chǔ)項(xiàng)
export const setItem = (k: string, v: any, expired: number = -1) => {
const expiredKey = `${k}__expires__`;
let exp = 0;
if (expired === NEVER_EXPIRES_FLAG) {
exp = NEVER_EXPIRES_FLAG;
} else if (expired >= 0) {
exp = Date.now() + 1000 * 60 * expired;
}
// 存儲(chǔ)過期時(shí)間
localforage.setItem(expiredKey, exp.toString()).catch((err) => {
console.error('設(shè)置過期時(shí)間失敗:', err);
});
// 存儲(chǔ)實(shí)際數(shù)據(jù)
return localforage.setItem(k, v);
};
- 參數(shù)說明:
k:鍵名。v:值。expired:過期時(shí)間,單位為分鐘,默認(rèn)為-1(永不過期)。
- 實(shí)現(xiàn)邏輯:
- 生成一個(gè)對(duì)應(yīng)的過期時(shí)間鍵名
expiredKey。 - 根據(jù)過期時(shí)間計(jì)算實(shí)際的過期時(shí)間戳
exp。- 如果
expired為-1,則設(shè)為NEVER_EXPIRES_FLAG。 - 如果
expired大于等于0,則計(jì)算未來的時(shí)間戳。
- 如果
- 使用
localforage.setItem分別存儲(chǔ)過期時(shí)間和實(shí)際數(shù)據(jù)。
- 生成一個(gè)對(duì)應(yīng)的過期時(shí)間鍵名
獲取存儲(chǔ)項(xiàng)
export const getItem = async (k: string) => {
const expiredKey = `${k}__expires__`;
try {
const expiredValue = await localforage.getItem<string | null>(expiredKey);
if (expiredValue === null) {
// 未設(shè)置過期時(shí)間,視為不存在
return null;
}
const expiredTime = parseInt(expiredValue, 10);
if (expiredTime === NEVER_EXPIRES_FLAG) {
// 永不過期
return localforage.getItem(k);
}
if (expiredTime > Date.now()) {
// 未過期,返回?cái)?shù)據(jù)
return localforage.getItem(k);
} else {
// 已過期,刪除數(shù)據(jù)
removeItem(k);
removeItem(expiredKey);
return null;
}
} catch (err) {
console.error('獲取數(shù)據(jù)失敗:', err);
return null;
}
};
- 實(shí)現(xiàn)邏輯:
- 先獲取對(duì)應(yīng)的過期時(shí)間
expiredValue。- 如果未設(shè)置過期時(shí)間,直接返回
null。
- 如果未設(shè)置過期時(shí)間,直接返回
- 將過期時(shí)間字符串轉(zhuǎn)換為數(shù)字
expiredTime。 - 根據(jù)過期時(shí)間判斷:
- 如果是永不過期標(biāo)志,直接返回?cái)?shù)據(jù)。
- 如果未過期(
expiredTime > Date.now()),返回?cái)?shù)據(jù)。 - 如果已過期,刪除數(shù)據(jù)并返回
null。
- 先獲取對(duì)應(yīng)的過期時(shí)間
刪除存儲(chǔ)項(xiàng)
export const removeItem = (k: string) => {
const expiredKey = `${k}__expires__`;
localforage.removeItem(expiredKey).catch((err) => {
console.error('刪除過期時(shí)間失敗:', err);
});
return localforage.removeItem(k);
};
- 實(shí)現(xiàn)邏輯:
- 同時(shí)刪除數(shù)據(jù)和對(duì)應(yīng)的過期時(shí)間。
使用示例
// 存儲(chǔ)數(shù)據(jù),設(shè)置過期時(shí)間為 5 分鐘
setItem('userInfo', { name: '張三', age: 28 }, 5);
// 獲取數(shù)據(jù)
getItem('userInfo').then((data) => {
if (data) {
console.log('用戶信息:', data);
} else {
console.log('用戶信息已過期或不存在');
}
});
// 刪除數(shù)據(jù)
removeItem('userInfo');
注意事項(xiàng)
- 異步操作:localForage 的所有方法都是異步的,返回的是 Promise,所以在獲取數(shù)據(jù)時(shí)需要使用
then或async/await。 - 數(shù)據(jù)類型:localForage 支持存儲(chǔ)多種數(shù)據(jù)類型,包括對(duì)象、數(shù)組、Blob 等。
- 錯(cuò)誤處理:在實(shí)際開發(fā)中,應(yīng)對(duì)可能出現(xiàn)的錯(cuò)誤進(jìn)行處理,以提高代碼的健壯性。
多實(shí)例存儲(chǔ)
上面的代碼實(shí)例全局只使用了一個(gè)實(shí)例存儲(chǔ),如果希望使用多實(shí)例存儲(chǔ),可以進(jìn)行簡(jiǎn)單的修改,下面是一個(gè)使用組合函數(shù)的方式實(shí)現(xiàn)多實(shí)例存儲(chǔ)的代碼。
import localforage from 'localforage';
export const useLocalforage = (options: LocalForageOptions ) => {
// 配置 localForage
const store = localforage.createInstance({
...options,
});
// 定義一個(gè)永不過期的標(biāo)志
const NEVER_EXPIRES_FLAG = -1;
/**
* 設(shè)置存儲(chǔ)項(xiàng)
* @param k 鍵名
* @param v 值
* @param expired 過期時(shí)間(分鐘),默認(rèn)永不過期
* @returns Promise
*/
const setItem = (k: string, v: any, expired: number = -1): Promise<void> => {
const expiredKey = `${k}__expires__`;
let exp = 0;
if (expired === NEVER_EXPIRES_FLAG) {
exp = NEVER_EXPIRES_FLAG;
} else if (expired >= 0) {
exp = Date.now() + 1000 * 60 * expired;
}
// 存儲(chǔ)過期時(shí)間
store.setItem(expiredKey, exp.toString()).catch((err) => {
console.error('設(shè)置過期時(shí)間失敗:', err);
});
// 存儲(chǔ)實(shí)際數(shù)據(jù)
return store.setItem(k, v);
};
/**
* 獲取存儲(chǔ)項(xiàng)
* @param k 鍵名
* @returns Promise<T | null>
*/
const getItem = async <T> (k: string) : Promise<T | null> => {
const expiredKey = `${k}__expires__`;
try {
const expiredValue = await store.getItem<string | null>(expiredKey);
if (expiredValue === null) {
// 未設(shè)置過期時(shí)間,視為不存在
return null;
}
const expiredTime = parseInt(expiredValue, 10);
if (expiredTime === NEVER_EXPIRES_FLAG) {
// 永不過期
return store.getItem(k) as T;
}
if (expiredTime > Date.now()) {
// 未過期,返回?cái)?shù)據(jù)
return store.getItem(k);
} else {
// 已過期,刪除數(shù)據(jù)
removeItem(k);
removeItem(expiredKey);
return null;
}
} catch (err) {
console.error('獲取數(shù)據(jù)失敗:', err);
return null;
}
};
/**
* 刪除存儲(chǔ)項(xiàng)
* @param k 鍵名
* @returns Promise
*/
const removeItem = (k: string) => {
const expiredKey = `${k}__expires__`;
store.removeItem(expiredKey).catch((err) => {
console.error('刪除過期時(shí)間失敗:', err);
});
return store.removeItem(k);
};
return {
getItem,
setItem,
}
}
export default useLocalforage;
使用示例
<script setup lang="ts">
import { onMounted, ref } from "vue";
import useLocalForage from "./use-local-forage";
import { USER_VISITOR_COUNT, SHOW_NAVIGATOR_BOOL } from "./storage-constants";
import Demo from './Demo.vue';
const localForageInstance = useLocalForage({
name: "test",
storeName: 'storeName'
});
const visitorCount = ref(0);
const loadStorage = async () => {
try {
const data = await localForageInstance.getItem<number>(USER_VISITOR_COUNT);
visitorCount.value = data || 0;
} catch (error) {
console.error(error);
} finally {
recordVisitorCount();
}
};
const recordVisitorCount = () => {
localForageInstance.setItem(USER_VISITOR_COUNT, visitorCount.value + 1);
};
onMounted(() => {
loadStorage();
})
</script>
<template>
<h1 v-show="visitorCount">用戶訪問次數(shù){{ visitorCount }}次</h1>
<Demo />
</template>
不同之處是使用const store = localforage.createInstance來創(chuàng)建實(shí)例,每次使用創(chuàng)建的store 來進(jìn)行操作,并且會(huì)根據(jù)命名來存放數(shù)據(jù),這對(duì)于分類管理數(shù)據(jù)非常有用。

當(dāng)然如果命名相同,就會(huì)存放在一個(gè)庫(kù)中,但建議根據(jù)功能來區(qū)分?jǐn)?shù)據(jù)。比如項(xiàng)目數(shù)據(jù)存放在一個(gè)庫(kù)中,數(shù)據(jù) mock 存放在另一個(gè)庫(kù)中。

總結(jié)
通過以上實(shí)現(xiàn),我們可以方便地使用 localForage 來存儲(chǔ)帶過期時(shí)間的數(shù)據(jù)。相比傳統(tǒng)的 localStorage,localForage 提供了更強(qiáng)大的功能和更好的性能,適用于大多數(shù)前端存儲(chǔ)場(chǎng)景。
本案例的所有代碼托管在:multi-localforage-demo
以上就是使用localForage實(shí)現(xiàn)帶過期時(shí)間的本地存儲(chǔ)方案的詳細(xì)內(nèi)容,更多關(guān)于localForage本地存儲(chǔ)方案的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
用JavaScript實(shí)現(xiàn)PHP的urlencode與urldecode函數(shù)
這篇文章主要介紹了用JavaScript實(shí)現(xiàn)PHP的urlencode與urldecode函數(shù),很多情況下我們用了出來php urlencode出來的網(wǎng)址,需要的朋友可以參考下2015-08-08
用window.onerror捕獲并上報(bào)Js錯(cuò)誤的方法
這篇文章主要介紹了用window.onerror捕獲并上報(bào)Js錯(cuò)誤的方法,需要的朋友可以參考下2016-01-01
BootStrap實(shí)現(xiàn)帶有增刪改查功能的表格(DEMO詳解)
這篇文章主要介紹了BootStrap實(shí)現(xiàn)帶有增刪改查功能的表格,表格封裝了3個(gè)版本,接下來通過本文給大家展示下樣式及代碼,對(duì)bootstrap增刪改查相關(guān)知識(shí)感興趣的朋友一起通過本文學(xué)習(xí)吧2016-10-10
js實(shí)現(xiàn)獲取兩個(gè)日期之間所有日期的方法
這篇文章主要介紹了js實(shí)現(xiàn)獲取兩個(gè)日期之間所有日期的方法,涉及javascript針對(duì)日期與時(shí)間的相關(guān)操作技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2016-06-06
js實(shí)現(xiàn)表單多按鈕提交action的處理方法
這篇文章主要介紹了js實(shí)現(xiàn)表單多按鈕提交action的處理方法,需要的朋友可以參考下2015-10-10

