本文你能學到
- 優雅的
Storage
工具類如何封裝(支持前綴key、加密存儲、過期時間,ts封裝等)
前言
localStorage
使用是一個老生常談的話題,本文不講解基礎 api
,主要教你如何封裝一個優雅的localStorage
工具,以及一些 localStorage
中一些你不知道的知識點。
優雅的 Storage 工具如何封裝(前綴、加密、過期時間等)
該工具函數設計
- 采用工廠方法+閉包設計模式,不直接實例化類,而是根據傳入的參數來配置和返回一個
SmartStorage
的實例。 - 支持帶前綴的鍵:通過
prefixKey
參數可以為存儲的鍵名添加一個前綴,默認為空字符串。這個功能可以幫助避免鍵名沖突,特別是當在同一個域下的不同應用或組件中使用同一種存儲方式時。 - 支持過期時間:在存儲數據時,可以為每項數據設置一個過期時間(單位為秒),存儲的數據結構中會包括實際的值、存儲時間戳以及過期時間戳。在讀取數據時,會檢查數據是否過期,如果已經過期,則自動刪除
- 支持加密存儲:存儲數據時根據參數配置可先進行加密,讀取數據時再解密,加密使用的
crypto
模塊 - 錯誤處理:在讀取數據時,如果解密過程出錯或數據格式不正確,會捕獲異常并返回默認值,這提高了程序的健壯性。
- 支持常用的
api
(set get remove clear
)
接下來是代碼實現:在未進行代碼實現前可以基于上面的設計自己實現一下,然后對照下我的代碼實現
/**
* 封裝一個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;
}
/**
* 加密類簡單實現
*/
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, // 加密部分不贅余,自行搜索參數學習
padding: pkcs7, // 加密部分不贅余,自行搜索參數學習
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工廠方法實現
* @param param0
* @returns
*/
export const createSmartStorage = ({
prefixKey = '',
storage = localStorage, // 這里其實也可以支持sessionStorage,自行配置
key = cacheConfig.key, // 修改為自己項目cacheConfig中的key
iv = cacheConfig.iv, // 修改為自己項目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,// 修改為自己項目cacheConfig中的key
iv: cacheConfig.iv,// 修改為自己項目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();
};
再補充幾個 localStorage
相關可能你不知道的知識點。
localStorage 存儲大小
localStorage
的存儲空間是 5M
,但是單位是字符串的長度值, 或者 utf-16
的編碼單元,也可以說是 10M
字節空間。localStorage
的 key
鍵也是占存儲空間的。
function sieOfLS() {
return Object.entries(localStorage).map(v => v.join('')).join('').length;
}
這個函數也可以加到storage工具函數中
localStorage.clear();
localStorage.setItem("🌞", 1);
localStorage.setItem("🌞🌞🌞🌞", 1111);
console.log("size:", sieOfLS()) // 15
// 🌞*5 + 1 *5 = 2*5 + 1*5 = 15
localStorage 如何監聽
window.addEventListener('storage', () => {
// callback
})
每次 localStorage
中有任何變動都會觸發一個 storage
事件,即使是同域下的不同頁面A、B都會監聽這個事件,一旦有窗口更新 localStorage
,其他窗口都會收到通知。
- 基于我們前面封裝的
localStorage
工具類 在封裝后每一個函數內部可以進行監聽,同時如果想要統計監聽一些內容,可以給一些函數增加 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
裝飾器相關內容可以看我另一篇文章,本文只講解 localStorage
localStorage 同源
只有來自同一個源的網頁才能訪問相同的 localStorage
對應 key
的數據,這也是前面工具類封裝,這個參數 prefixKey
的作用,同源項目可以加一個唯一 key
,保證同源下的 localStorage
不沖突。
這是一個需要避免的問題,有時候也會基于這些實現一些功能,比如下面的同源窗口通信
同源窗口通信
我們就可以只有一個窗口與后臺建立連接,收到更新后,廣播給其他窗口就可以。想象這樣一個場景:當localStorage
中的數據發生變化時,瀏覽器會觸發一個 storage
事件,這個事件能夠被同一源下所有的窗口監聽到。這意味著,如果一個窗口更新了 localStorage
,其他窗口可以實時接收到這一變動的通知。雖然這個機制的原理相對簡單——基于事件的廣播,但是要搭建一個功能完備的跨窗口通信機制,則需要考慮更多的細節和挑戰。
- 每個窗口都需要有一個獨一無二的標識符
(ID)
,以便在眾多窗口中準確識別和管理它們。 - 為了避免同一個消息被重復處理,必須有機制確保消息的唯一性。
- 還需要確保只有那些真正需要接收特定消息的窗口才會收到通知,這就要求消息分發機制能夠有效地過濾掉不相關的窗口。
- 考慮到窗口可能會因為各種原因關閉或變得不響應,引入窗口的“心跳”機制來監控它們的活躍狀態變得尤為重要。
- 當涉及到需要從多個窗口中選舉出一個主窗口來協調操作時,主窗口的選舉機制也是不可或缺的一環。
盡管這些要求聽起來可能令人望而卻步,不過開源社區已經提供了一些優秀的解決方案來簡化這個過程。例如,diy/intercom
.js和tejacques/crosstab
這兩個庫就是專門為了解決跨窗口通信而設計的。感興趣的學習下
擴展知識 同源策略
協議相同:網頁地址的協議必須相同。例如,http://example.com
和 https://example.com
被視為不同的源,因此如果一個頁面是通過 HTTP
加載的,它不能訪問通過HTTPS加載的頁面的 LocalStorage
數據,反之亦然。
域名相同:網頁的域名必須完全相同。子域與主域也被視為不同的源(例如,sub.example.com
與 example.com
),默認情況下,它們無法共享 LocalStorage
數據。
端口相同:即使協議和域名相同,端口號的不同也會使它們成為不同的源。例如,http://example.com:80
和 http://example.com:8080
是不同的源。
總結
相信學習完本文你對 LocalStorage
有一個徹底的掌握,創作不易歡迎三連支持。
該文章在 2024/4/12 23:17:25 編輯過