使用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ǔ)庫,提供了異步的 API,支持存儲(chǔ)多種類型的數(shù)據(jù)(如對(duì)象、數(shù)組、二進(jìn)制數(shù)據(jù)等),并且在內(nèi)部優(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)行簡單的修改,下面是一個(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è)庫中,但建議根據(jù)功能來區(qū)分?jǐn)?shù)據(jù)。比如項(xiàng)目數(shù)據(jù)存放在一個(gè)庫中,數(shù)據(jù) mock 存放在另一個(gè)庫中。
總結(jié)
通過以上實(shí)現(xiàn),我們可以方便地使用 localForage 來存儲(chǔ)帶過期時(shí)間的數(shù)據(jù)。相比傳統(tǒng)的 localStorage
,localForage 提供了更強(qiáng)大的功能和更好的性能,適用于大多數(shù)前端存儲(chǔ)場景。
本案例的所有代碼托管在:multi-localforage-demo
以上就是使用localForage實(shí)現(xiàn)帶過期時(shí)間的本地存儲(chǔ)方案的詳細(xì)內(nèi)容,更多關(guān)于localForage本地存儲(chǔ)方案的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Select下拉框模糊查詢功能實(shí)現(xiàn)代碼
這篇文章主要介紹了Select下拉框模糊查詢功能實(shí)現(xiàn)代碼的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-07-07javascript的函數(shù)、創(chuàng)建對(duì)象、封裝、屬性和方法、繼承
從一開始接觸到j(luò)s就感覺好靈活,每個(gè)人的寫法都不一樣,比如一個(gè)function就有N種寫法2011-03-03前端滾動(dòng)錨點(diǎn)三個(gè)常用方案(點(diǎn)擊后頁面滾動(dòng)到指定位置)
這篇文章主要給大家介紹了關(guān)于前端滾動(dòng)錨點(diǎn)的三個(gè)常用方案,實(shí)現(xiàn)的效果就是點(diǎn)擊后頁面滾動(dòng)到指定位置,三種方法分別是scrollIntoView、scrollTo和scrollBy,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-01-01JavaScript中具名函數(shù)的多種調(diào)用方式總結(jié)
這篇文章主要介紹了JavaScript中具名函數(shù)的多種調(diào)用方式總結(jié),本文總結(jié)了4種方法,需要的朋友可以參考下2014-11-11JS使用for循環(huán)遍歷Table的所有單元格內(nèi)容
JS遍歷Table的所有單元格內(nèi)容思路是遍歷Table的所有Row,遍歷Row中的每一列,獲取Table中單元格的內(nèi)容2014-08-08JavaScript ( (__ = !$ + $)[+$] + ({} + $)[_/_] +({} + $)[_/_
今天在網(wǎng)上看到一篇很有意思的文章(需翻墻),解釋了幾段非常有趣的 JavaScript 代碼。你猜下面這段 JavaScript 代碼是什么意思?2011-02-02