本文你能學(xué)到
- 優(yōu)雅的
Storage
工具類如何封裝(支持前綴key、加密存儲、過期時(shí)間,ts封裝等) localStorage
真實(shí)存儲大小/存儲統(tǒng)計(jì)
前言
localStorage
使用是一個(gè)老生常談的話題,本文不講解基礎(chǔ) api
,主要教你如何封裝一個(gè)優(yōu)雅的localStorage
工具,以及一些 localStorage
中一些你不知道的知識點(diǎn)。
優(yōu)雅的 Storage 工具如何封裝(前綴、加密、過期時(shí)間等)
該工具函數(shù)設(shè)計(jì)
- 采用工廠方法+閉包設(shè)計(jì)模式,不直接實(shí)例化類,而是根據(jù)傳入的參數(shù)來配置和返回一個(gè)
SmartStorage
的實(shí)例。 - 支持帶前綴的鍵:通過
prefixKey
參數(shù)可以為存儲的鍵名添加一個(gè)前綴,默認(rèn)為空字符串。這個(gè)功能可以幫助避免鍵名沖突,特別是當(dāng)在同一個(gè)域下的不同應(yīng)用或組件中使用同一種存儲方式時(shí)。 - 支持過期時(shí)間:在存儲數(shù)據(jù)時(shí),可以為每項(xiàng)數(shù)據(jù)設(shè)置一個(gè)過期時(shí)間(單位為秒),存儲的數(shù)據(jù)結(jié)構(gòu)中會包括實(shí)際的值、存儲時(shí)間戳以及過期時(shí)間戳。在讀取數(shù)據(jù)時(shí),會檢查數(shù)據(jù)是否過期,如果已經(jīng)過期,則自動刪除
- 支持加密存儲:存儲數(shù)據(jù)時(shí)根據(jù)參數(shù)配置可先進(jìn)行加密,讀取數(shù)據(jù)時(shí)再解密,加密使用的
crypto
模塊 - 錯(cuò)誤處理:在讀取數(shù)據(jù)時(shí),如果解密過程出錯(cuò)或數(shù)據(jù)格式不正確,會捕獲異常并返回默認(rèn)值,這提高了程序的健壯性。
- 支持常用的
api
(set get remove clear
)
接下來是代碼實(shí)現(xiàn):在未進(jìn)行代碼實(shí)現(xiàn)前可以基于上面的設(shè)計(jì)自己實(shí)現(xiàn)一下,然后對照下我的代碼實(shí)現(xiàn)
/**
* 封裝一個(gè)local
*/
import { decrypt as aesDecrypt, encrypt as aesEncrypt } from 'crypto-js/aes';
import UTF8, { parse } from 'crypto-js/enc-utf8';
import pkcs7 from 'crypto-js/pad-pkcs7';
import CTR from 'crypto-js/mode-ctr';
import {isNil} from 'lodash';
interface EncryptionParams {
key: string;
iv: string;
}
export interface Encryption {
encrypt(plainText: string): string;
decrypt(cipherText: string): string;
}
/**
* 加密類簡單實(shí)現(xiàn)
*/
class AesEncryption implements Encryption {
private readonly key;
private readonly iv;
constructor({ key, iv }: EncryptionParams) {
this.key = parse(key);
this.iv = parse(iv);
}
get getOptions() {
return {
mode: CTR, // 加密部分不贅余,自行搜索參數(shù)學(xué)習(xí)
padding: pkcs7, // 加密部分不贅余,自行搜索參數(shù)學(xué)習(xí)
iv: this.iv,
};
}
encrypt(plainText: string) {
return aesEncrypt(plainText, this.key, this.getOptions).toString();
}
decrypt(cipherText: string) {
return aesDecrypt(cipherText, this.key, this.getOptions).toString(UTF8);
}
}
export interface CreateSmartStorageParams extends EncryptionParams {
prefixKey: string;
storage: Storage;
hasEncrypt: boolean;
timeout?: number;
}
/**
* localStorage工廠方法實(shí)現(xiàn)
* @param param0
* @returns
*/
export const createSmartStorage = ({
prefixKey = '',
storage = localStorage, // 這里其實(shí)也可以支持sessionStorage,自行配置
key = cacheConfig.key, // 修改為自己項(xiàng)目cacheConfig中的key
iv = cacheConfig.iv, // 修改為自己項(xiàng)目cacheConfig中的iv
timeout = null,
hasEncrypt = true,
}: Partial<CreateSmartStorageParams> = {}) => {
if (hasEncrypt && [key.length, iv.length].some((item) => item !== 16)) {
throw new Error('When hasEncrypt is true, the key or iv must be 16 bits!');
}
//
const persistEncryption: Encryption = new AesEncryption({
key: cacheConfig.key,// 修改為自己項(xiàng)目cacheConfig中的key
iv: cacheConfig.iv,// 修改為自己項(xiàng)目cacheConfig中的iv
})
/**
* Cache class
* Construction parameters can be passed intolocalStorage,
* @class Cache
* @example
*/
const SmartStorage = class SmartStorage {
private storage: Storage;
private prefixKey?: string;
private encryption: Encryption;
private hasEncrypt: boolean;
/**
*
* @param {*} storage
*/
constructor() {
this.storage = storage;
this.prefixKey = prefixKey;
this.encryption = persistEncryption;
this.hasEncrypt = hasEncrypt;
}
private getKey(key: string) {
return `${this.prefixKey}${key}`.toUpperCase();
}
/**
* Set cache
* @param {string} key
* @param {*} value
* @param {*} expire Expiration time in seconds
* @memberof Cache
*/
set(key: string, value: any, expire: number | null = timeout) {
const stringData = JSON.stringify({
value,
time: Date.now(),
expire: !isNil(expire) ? new Date().getTime() + expire * 1000 : null,
});
const stringifyValue = this.hasEncrypt ? this.encryption.encrypt(stringData) : stringData;
this.storage.setItem(this.getKey(key), stringifyValue);
}
/**
* Read cache
* @param {string} key
* @param {*} def
* @memberof Cache
*/
get(key: string, def: any = null): any {
const val = this.storage.getItem(this.getKey(key));
if (!val) return def;
try {
const decVal = this.hasEncrypt ? this.encryption.decrypt(val) : val;
const data = JSON.parse(decVal);
const { value, expire } = data;
if (isNil(expire) || expire >= new Date().getTime()) {
return value;
}
this.remove(key);
} catch (e) {
return def;
}
}
/**
* Delete cache based on key
* @param {string} key
* @memberof Cache
*/
remove(key: string) {
this.storage.removeItem(this.getKey(key));
}
/**
* Delete all caches of this instance
*/
clear(): void {
this.storage.clear();
}
};
return new SmartStorage();
};
再補(bǔ)充幾個(gè) localStorage
相關(guān)可能你不知道的知識點(diǎn)。
localStorage 存儲大小
localStorage
的存儲空間是 5M
,但是單位是字符串的長度值, 或者 utf-16
的編碼單元,也可以說是 10M
字節(jié)空間。localStorage
的 key
鍵也是占存儲空間的。localStorage
如何統(tǒng)計(jì)已使用空間
function sieOfLS() {
return Object.entries(localStorage).map(v => v.join('')).join('').length;
}
這個(gè)函數(shù)也可以加到storage工具函數(shù)中
localStorage.clear();
localStorage.setItem("🌞", 1);
localStorage.setItem("🌞🌞🌞🌞", 1111);
console.log("size:", sieOfLS()) // 15
// 🌞*5 + 1 *5 = 2*5 + 1*5 = 15
localStorage 如何監(jiān)聽
window.addEventListener('storage', () => {
// callback
})
每次 localStorage
中有任何變動都會觸發(fā)一個(gè) storage
事件,即使是同域下的不同頁面A、B都會監(jiān)聽這個(gè)事件,一旦有窗口更新 localStorage
,其他窗口都會收到通知。
- 基于我們前面封裝的
localStorage
工具類 在封裝后每一個(gè)函數(shù)內(nèi)部可以進(jìn)行監(jiān)聽,同時(shí)如果想要統(tǒng)計(jì)監(jiān)聽一些內(nèi)容,可以給一些函數(shù)增加 aop
裝飾器來完成。
@aop
set(key: string, value: any, expire: number | null = timeout) {
const stringData = JSON.stringify({
value,
time: Date.now(),
expire: !isNil(expire) ? new Date().getTime() + expire * 1000 : null,
});
const stringifyValue = this.hasEncrypt ? this.encryption.encrypt(stringData) : stringData;
this.storage.setItem(this.getKey(key), stringifyValue);
}
具體 aop
裝飾器相關(guān)內(nèi)容可以看我另一篇文章,本文只講解 localStorage
localStorage 同源
只有來自同一個(gè)源的網(wǎng)頁才能訪問相同的 localStorage
對應(yīng) key
的數(shù)據(jù),這也是前面工具類封裝,這個(gè)參數(shù) prefixKey
的作用,同源項(xiàng)目可以加一個(gè)唯一 key
,保證同源下的 localStorage
不沖突。
這是一個(gè)需要避免的問題,有時(shí)候也會基于這些實(shí)現(xiàn)一些功能,比如下面的同源窗口通信
同源窗口通信
我們就可以只有一個(gè)窗口與后臺建立連接,收到更新后,廣播給其他窗口就可以。想象這樣一個(gè)場景:當(dāng)localStorage
中的數(shù)據(jù)發(fā)生變化時(shí),瀏覽器會觸發(fā)一個(gè) storage
事件,這個(gè)事件能夠被同一源下所有的窗口監(jiān)聽到。這意味著,如果一個(gè)窗口更新了 localStorage
,其他窗口可以實(shí)時(shí)接收到這一變動的通知。雖然這個(gè)機(jī)制的原理相對簡單——基于事件的廣播,但是要搭建一個(gè)功能完備的跨窗口通信機(jī)制,則需要考慮更多的細(xì)節(jié)和挑戰(zhàn)。
- 每個(gè)窗口都需要有一個(gè)獨(dú)一無二的標(biāo)識符
(ID)
,以便在眾多窗口中準(zhǔn)確識別和管理它們。 - 為了避免同一個(gè)消息被重復(fù)處理,必須有機(jī)制確保消息的唯一性。
- 還需要確保只有那些真正需要接收特定消息的窗口才會收到通知,這就要求消息分發(fā)機(jī)制能夠有效地過濾掉不相關(guān)的窗口。
- 考慮到窗口可能會因?yàn)楦鞣N原因關(guān)閉或變得不響應(yīng),引入窗口的“心跳”機(jī)制來監(jiān)控它們的活躍狀態(tài)變得尤為重要。
- 當(dāng)涉及到需要從多個(gè)窗口中選舉出一個(gè)主窗口來協(xié)調(diào)操作時(shí),主窗口的選舉機(jī)制也是不可或缺的一環(huán)。
盡管這些要求聽起來可能令人望而卻步,不過開源社區(qū)已經(jīng)提供了一些優(yōu)秀的解決方案來簡化這個(gè)過程。例如,diy/intercom
.js和tejacques/crosstab
這兩個(gè)庫就是專門為了解決跨窗口通信而設(shè)計(jì)的。感興趣的學(xué)習(xí)下
擴(kuò)展知識 同源策略
協(xié)議相同:網(wǎng)頁地址的協(xié)議必須相同。例如,http://example.com
和 https://example.com
被視為不同的源,因此如果一個(gè)頁面是通過 HTTP
加載的,它不能訪問通過HTTPS加載的頁面的 LocalStorage
數(shù)據(jù),反之亦然。
域名相同:網(wǎng)頁的域名必須完全相同。子域與主域也被視為不同的源(例如,sub.example.com
與 example.com
),默認(rèn)情況下,它們無法共享 LocalStorage
數(shù)據(jù)。
端口相同:即使協(xié)議和域名相同,端口號的不同也會使它們成為不同的源。例如,http://example.com:80
和 http://example.com:8080
是不同的源。
總結(jié)
相信學(xué)習(xí)完本文你對 LocalStorage
有一個(gè)徹底的掌握,創(chuàng)作不易歡迎三連支持。
該文章在 2024/4/12 23:17:25 編輯過