面試官: 如何讓localStorage支持過期時(shí)間設(shè)置?
聊到 localStorage 想必熟悉前端的朋友都不會(huì)陌生, 我們可以使用它提供的 getItem, setItem, removeItem, clear 這幾個(gè) API 輕松的對(duì)存儲(chǔ)在瀏覽器本地的數(shù)據(jù)進(jìn)行讀,寫, 刪操作, 但是相比于 cookie, localStorage 唯一美中不足的就是不能設(shè)置每一個(gè)鍵的過期時(shí)間。
localStorage 屬性允許我們?cè)L問一個(gè) Document 源(origin)的對(duì)象 Storage;存儲(chǔ)的數(shù)據(jù)將保存在瀏覽器會(huì)話中。localStorage 類似 sessionStorage,但其區(qū)別在于:存儲(chǔ)在 localStorage 的數(shù)據(jù)可以長(zhǎng)期保留;而當(dāng)頁(yè)面會(huì)話結(jié)束——也就是說,當(dāng)頁(yè)面被關(guān)閉時(shí),存儲(chǔ)在 sessionStorage 的數(shù)據(jù)會(huì)被清除 。
我們還應(yīng)注意,localStorage 中的鍵值對(duì)總是以字符串的形式存儲(chǔ)。
問題描述
在實(shí)際的應(yīng)用場(chǎng)景中, 我們往往需要讓 localStorage 設(shè)置的某個(gè) key 能在指定時(shí)間內(nèi)自動(dòng)失效, 所以基于這種場(chǎng)景, 我們?nèi)绾稳ソ鉀Q呢?
1. 初級(jí)解法
對(duì)于剛熟悉前端的朋友, 可能會(huì)立馬給出答案:
- localStorage.setItem('dooring', '1.0.0')
- // 設(shè)置一小時(shí)的有效期
- const expire = 1000 * 60 * 60;
- setTimeout(() => {
- localStorage.setItem('dooring', '')
- }, expire)
當(dāng)然這種方案能解決一時(shí)的問題, 但是如果要設(shè)置任意鍵的有效期, 使用這種方案就需要編寫多個(gè)定時(shí)器, 維護(hù)成本極高, 且不利于工程化復(fù)用。
2. 中級(jí)解法
前端工程師在有一定的工作經(jīng)驗(yàn)之后, 往往會(huì)去考慮工程化和復(fù)用性的問題, 并對(duì)數(shù)據(jù)結(jié)構(gòu)有了一定的了解, 所以可能會(huì)有接下來的解法:
- 用localStorage存一份{key(鍵): expire(過期時(shí)間)}的映射表
- 重寫localStorage API, 對(duì)方法進(jìn)行二次封裝
類似的代碼如下:
- const store = {
- // 存儲(chǔ)過期時(shí)間映射
- setExpireMap: (key, expire) => {
- const expireMap = localStorage.getItem('EXPIRE_MAP') || "{}"
- localStorage.setItem(
- 'EXPIRE_MAP',
- JSON.stringify({
- ...JSON.parse(expireMap),
- key: expire
- }))
- },
- setItem: (key, value, expire) => {
- store.setExpireMap(key, expire)
- localStorage.setItem(key, value)
- },
- getItem: (key) => {
- // 在取值之前先判斷是否過期
- const expireMap = JSON.parse(
- localStorage.getItem('EXPIRE_MAP') || "{}"
- )
- if(expireMap[key] && expireMap[key] < Date.now()) {
- return localStorage.getItem(key)
- }else {
- localStorage.removeItem(key)
- return null
- }
- }
- // ...
- }
眨眼一看這個(gè)方案確實(shí)解決了復(fù)用性的問題, 并且不同團(tuán)隊(duì)都可以使用這個(gè)方案, 但仍然有一些缺點(diǎn):
- 對(duì) store 操作時(shí)需要維護(hù)2份數(shù)據(jù), 并且占用緩存空間
- 如果 EXPIRE_MAP 誤刪除將會(huì)導(dǎo)致所有過期時(shí)間失效
- 對(duì)操作過程缺少更靈活的控制(比如操作狀態(tài), 操作回調(diào)等)
3. 高級(jí)解法
為了減少維護(hù)成本和空間占用, 并支持一定的靈活控制和容錯(cuò)能力, 我們又應(yīng)該怎么做呢?
這里筆者想到了兩種類似的方案:
- 將過期時(shí)間存到 key 中, 如 dooring|6000, 每次取值時(shí)通過分隔符“|”來將 key 和 expire 取出, 進(jìn)行判斷
- 將過期時(shí)間存到 value 中, 如 1.0.0|6000, 剩下的同1
為了更具有封裝性和可靠性, 我們還可以配置不同狀態(tài)下的回調(diào), 簡(jiǎn)單實(shí)現(xiàn)如下:
- const store = {
- preId: 'xi-',
- timeSign: '|-door-|',
- status: {
- SUCCESS: 0,
- FAILURE: 1,
- OVERFLOW: 2,
- TIMEOUT: 3,
- },
- storage: localStorage || window.localStorage,
- getKey: function (key: string) {
- return this.preId + key;
- },
- set: function (
- key: string,
- value: string | number,
- time?: Date & number,
- cb?: (status: number, key: string, value: string | number) => void,
- ) {
- let _status = this.status.SUCCESS,
- _key = this.getKey(key),
- _time;
- // 設(shè)置失效時(shí)間,未設(shè)置時(shí)間默認(rèn)為一個(gè)月
- try {
- _time = time
- ? new Date(time).getTime() || time.getTime()
- : new Date().getTime() + 1000 * 60 * 60 * 24 * 31;
- } catch (e) {
- _time = new Date().getTime() + 1000 * 60 * 60 * 24 * 31;
- }
- try {
- this.storage.setItem(_key, _time + this.timeSign + value);
- } catch (e) {
- _status = this.status.OVERFLOW;
- }
- cb && cb.call(this, _status, _key, value);
- },
- get: function (
- key: string,
- cb?: (status: number, value: string | number | null) => void,
- ) {
- let status = this.status.SUCCESS,
- _key = this.getKey(key),
- value = null,
- timeSignLen = this.timeSign.length,
- that = this,
- index,
- time,
- result;
- try {
- value = that.storage.getItem(_key);
- } catch (e) {
- result = {
- status: that.status.FAILURE,
- value: null,
- };
- cb && cb.call(this, result.status, result.value);
- return result;
- }
- if (value) {
- index = value.indexOf(that.timeSign);
- time = +value.slice(0, index);
- if (time > new Date().getTime() || time == 0) {
- value = value.slice(index + timeSignLen);
- } else {
- (value = null), (status = that.status.TIMEOUT);
- that.remove(_key);
- }
- } else {
- status = that.status.FAILURE;
- }
- result = {
- status: status,
- value: value,
- };
- cb && cb.call(this, result.status, result.value);
- return result;
- },
- // ...
- };
- export default store;
這樣, 我們就實(shí)現(xiàn)了每個(gè) key 都有獨(dú)立的過期時(shí)間, 并且對(duì)不同的操作結(jié)果可以輕松的進(jìn)行狀態(tài)管控啦~
4. 骨灰級(jí)解法
當(dāng)然, 骨灰級(jí)解法是直接使用 xijs 這個(gè) javascript 工具庫(kù), 因?yàn)槲乙呀?jīng)將上述完整實(shí)現(xiàn)方案封裝到該庫(kù)中了, 我們只需要使用如下的方案, 就能輕松使用具有過期時(shí)間的強(qiáng)大的 localStorage 方法啦 :
- // 先安裝 yarn add xijs
- import { store } from 'xijs';
- // 設(shè)置帶有過期時(shí)間的key
- store.set('name', 'dooring', Date.now() + 1000);
- console.log(store.get('name'));
- setTimeout(() => {
- console.log(store.get('name'));
- }, 1000);
- // 設(shè)置成功后的回調(diào)
- store.set('dooring', 'xuxiaoxi', Date.now() + 1000, (status, key, value) => {
- console.log('success');
- });
同時(shí) xijs 還在持續(xù)擴(kuò)充更有用的工具函數(shù), 讓業(yè)務(wù)開發(fā)更高效. 目前已集成了如下工具函數(shù):
- store 基于 localStorage 上層封裝的支持過期時(shí)間設(shè)置的緩存庫(kù), 支持操作回調(diào)
- uuid 生成唯一id, 支持設(shè)置長(zhǎng)度
- randomStr 生成指定個(gè)數(shù)的隨機(jī)字符串
- formatDate 開箱即用的時(shí)間格式化工具
- debounce 防抖函數(shù)
- throttle 節(jié)流函數(shù)
- url2obj 將url字符串轉(zhuǎn)換為對(duì)象
- obj2url 將對(duì)象轉(zhuǎn)換成編碼后的url字符串
- isPC 判斷設(shè)備是否為PC類型
本文轉(zhuǎn)載自微信公眾號(hào)「趣談前端」
【編輯推薦】