需求簡介
在前端開發中,localStorage
和 sessionStorage
是非常常見的數據存儲解決方案。但在某些特殊場景下,原生的 localStorage
和 sessionStorage
無法滿足業務需求,例如:
- 「業務定制化需求」:需要在存儲和獲取某些特定鍵時加入邏輯,比如數據加密、校驗或默認值填充。
- 「全局監控」:希望對存儲和讀取操作進行監控,例如記錄關鍵數據的訪問日志或統計操作頻率。
- 「系統數據保護」:防止外部代碼對特定鍵值的誤改動。
在上面的場景中,我們通過重寫原生的 localStorage
和 sessionStorage
的方法,就可以實現這些特殊的需求。
技術方案
核心思路
要重寫window上原生的方法,我們要先將原生的 setItem
和 getItem
方法保留下來,以便在需要時調用。然后,通過下面的偽代碼重寫方法,在存儲或讀取過程中加入自定義邏輯。
const _setItem = localStorage.setItem;
localStorage.setItem = function (...args) {
// 自定義邏輯....
// 最終調用_setItem
};
最后,我們也可以提供恢復原方法的機制,確保代碼可控,不影響其他功能。
由于我們的重寫的是window
上的方法,因此,重寫的時機一定「要盡可能的早」。比如,我們使用的是vue項目,我們就應該在vue實例創建前,實現原生方法的重寫:
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
// 代理 localStorage 和 sessionStorage 方法
function proxyStorage(storage) {
// ...
}
// 代理 localStorage 和 sessionStorage
proxyStorage(localStorage);
proxyStorage(sessionStorage);
// 創建 Vue 應用
const app = createApp(App);
// 使用路由和狀態管理
app.use(router).use(store);
// 掛載應用
代理存儲方法
初步實現:簡單攔截
我們可以實現一個簡單的代理,針對特定鍵值在存儲和讀取時加入邏輯(根據業務而定)。例如:
function proxyStorage(storage) {
// 保存原始方法
const originalSetItem = storage.setItem;
const originalGetItem = storage.getItem;
// 重寫方法
storage.setItem = function (key, value) {
// 自定義邏輯,比如拒絕用戶修改system屬性
if (key === 'system') {
retrun
}
originalSetItem.call(this, key, value);
};
storage.getItem = function (key) {
// 自定義邏輯,比如用戶讀取system屬性,始終返回固定值
if (key === 'system') {
return "對不起,你無權讀取用戶信息"
}
return originalGetItem.call(this, key);
};
}
// 代理 localStorage 和 sessionStorage
proxyStorage(localStorage);
proxyStorage(sessionStorage);
上述代碼很簡單,你可能有疑問的就是為什么調用原生的方法時,我們要使用call?
originalSetItem.call(this, key, value);
這是因為originalGetItem
和 originalSetItem
是從 localStorage
或 sessionStorage
的原型方法保存下來的引用。如果直接調用 originalSetItem(key, value)
或 originalGetItem('origin_system')
,它們的上下文(this
)會丟失。
const setItem = localStorage.setItem;
setItem('key', 'value'); // 會報錯:Cannot read properties of undefined
這是因為 setItem
的上下文丟失,它不再知道自己屬于 localStorage
。
提供靈活的配置能力
為了應對更多場景需求,我們可以引入配置選項,讓代理邏輯更加靈活,比如,加入自定義鉤子函數,允許用戶自定義重寫的邏輯。
function proxyStorage(storage, config = {}) {
const originalSetItem = storage.setItem;
const originalGetItem = storage.getItem;
// 提供給用戶的鉤子函數
const beforeSetItem = config.beforeSetItem || ((key, value) => [key, value]);
const afterGetItem = config.afterGetItem || ((key, value) => value);
storage.setItem = function (key, value) {
// 調用用戶定義的 beforeSetItem 鉤子
const [newKey, newValue] = beforeSetItem(key, value) || [key, value];
if (newKey !== undefined && newValue !== undefined) {
originalSetItem.call(this, newKey, newValue);
}esle{
originalSetItem.call(this, key, value);
}
};
storage.getItem = function (key) {
const originalValue = originalGetItem.call(this, key);
// 調用用戶定義的 afterGetItem 鉤子
return afterGetItem(key, originalValue);
};
}
上述代碼中,beforeSetItem、afterGetItem是我們自定義鉤子函數,可以實現自定義返回值、讀取值的邏輯。我們看看它有什么實際使用場景:
「示例 1:加密存儲數據」
import CryptoJS from 'crypto-js';
const secretKey = '私有加密秘鑰';
proxyStorage(localStorage, {
beforeSetItem: (key, value) => {
const encryptedValue = CryptoJS.AES.encrypt(value, secretKey).toString();
return [key, encryptedValue];
},
afterGetItem: (key, value) => {
try {
const bytes = CryptoJS.AES.decrypt(value, secretKey);
return bytes.toString(CryptoJS.enc.Utf8) || null;
} catch (error) {
return null;
}
},
});
// 使用代理后的 localStorage
localStorage.setItem('sensitiveData', 'my-secret-data'); // 數據將被加密存儲
console.log(localStorage.getItem('sensitiveData')); // 數據將被解密返回
上述代碼實現了在存儲數據時加密,在讀取數據時解密的功能,非常具有實用價值!
「示例 2:監控存儲操作」
「記錄存儲和讀取行為:」
proxyStorage(localStorage, {
beforeSetItem: (key, value) => {
console.log(`設置值: key=${key}, value=${value}`);
// 設置值的其他記錄邏輯
return [key, value]; // 不修改原始行為
},
afterGetItem: (key, value) => {
console.log(`讀取值: key=${key}, value=${value}`);
//讀取值的其他記錄邏輯
return value; // 不修改原始行為
},
});
// 使用代理后的 localStorage
localStorage.setItem('exampleKey', 'exampleValue');
console.log(localStorage.getItem('exampleKey'));
「示例 3:攔截特定鍵值」
阻止某些特定鍵的存儲或讀取:
proxyStorage(localStorage, {
beforeSetItem: (key, value) => {
if (key === 'admin') {
console.warn(`您無權操作`);
return; // 攔截存儲操作
}
return [key, value];
},
afterGetItem: (key, value) => {
if (key === 'admin') {
console.warn(`您無權操作`);
return 'error'; // 返回自定義值
}
return value;
},
});
// 使用代理后的 localStorage
localStorage.setItem('admin', 'secretValue'); // 被攔截
console.log(localStorage.getItem('admin')); // 輸出: error
取消代理
在某些場景,我們可能需要取消代理,比如,當我們從A頁面切換到B頁面時,我們可能需要終止代理。因此,我們需要提供一個終止代理的方法。
function proxyStorage(storage, config = {}) {
const originalSetItem = storage.setItem;
const originalGetItem = storage.getItem;
// 提供給用戶的鉤子函數
const beforeSetItem = config.beforeSetItem || ((key, value) => [key, value]);
const afterGetItem = config.afterGetItem || ((key, value) => value);
storage.setItem = function (key, value) {
// 調用用戶定義的 beforeSetItem 鉤子
const [newKey, newValue] = beforeSetItem(key, value) || [key, value];
if (newKey !== undefined && newValue !== undefined) {
originalSetItem.call(this, newKey, newValue);
}esle{
originalSetItem.call(this, key, value);
}
};
storage.getItem = function (key) {
const originalValue = originalGetItem.call(this, key);
// 調用用戶定義的 afterGetItem 鉤子
return afterGetItem(key, originalValue);
};
const unproxy = () => {
storage.setItem = originalSetItem;
storage.getItem = originalGetItem;
};
return unproxy;
}
使用示例
// 代理 localStorage
const unproxy = proxyStorage(localStorage, config);
// 使用 localStorage
localStorage.setItem('key', '12345'); // 被攔截
// 恢復原始方法
unproxyLocalStorage();
整合后的最終代碼
我們可以將這個方法直接封裝成一個類,方便調用
class StorageProxy {
constructor(storage, config = {}) {
if (StorageProxy.instance) {
return StorageProxy.instance; // 返回已存在的實例
}
this.storage = storage;
this.config = config;
// 保存原始方法
this.originalSetItem = storage.setItem;
this.originalGetItem = storage.getItem;
// 提供默認的鉤子函數
this.beforeSetItem = config.beforeSetItem || ((key, value) => [key, value]);
this.afterGetItem = config.afterGetItem || ((key, value) => value);
// 初始化代理方法
this.proxyMethods();
// 緩存當前實例
StorageProxy.instance = this;
}
proxyMethods() {
const { storage, beforeSetItem, afterGetItem, originalSetItem, originalGetItem } = this;
storage.setItem = function (key, value) {
const [newKey, newValue] = beforeSetItem(key, value) || [key, value];
if (newKey !== undefined && newValue !== undefined) {
originalSetItem.call(this, newKey, newValue);
}
};
storage.getItem = function (key) {
const originalValue = originalGetItem.call(this, key);
return afterGetItem(key, originalValue);
};
}
unproxy() {
const { storage, originalSetItem, originalGetItem } = this;
storage.setItem = originalSetItem;
storage.getItem = originalGetItem;
}
static getInstance(storage = localStorage, config = {}) {
if (!StorageProxy.instance) {
new StorageProxy(storage, config);
}
return StorageProxy.instance;
}
}
export default StorageProxy;
注意,我們將 StorageProxy
封裝為單例模式可以確保整個應用中只有一個實例被創建和使用。
在 Vue 3 中的調用示例:
創建一個單獨的文件,比如 storageProxy.js
mport StorageProxy from './StorageProxy';
// 配置鉤子函數
const config = {
beforeSetItem: (key, value) => {
// ....
return [key, value];
},
afterGetItem: (key, value) => {
// ....
return value;
},
};
// 創建單例
const storageProxy = StorageProxy.getInstance(localStorage, config);
export default storageProxy;
在 main.js
中使用單例
將單例注入到 Vue 實例中,便于全局訪問:
import { createApp } from 'vue';
import App from './App.vue';
import storageProxy from './storageProxy';
const app = createApp(App);
// 注入全局屬性,供組件使用
app.config.globalProperties.$storageProxy = storageProxy;
app.mount('#app');
總結
本文給大家介紹了通過代理 localStorage
和 sessionStorage
實現自定義存儲邏輯,滿足特定業務需求、全局監控和數據保護等場景。
核心思路是重寫原生的 setItem
和 getItem
方法,并通過鉤子函數提供靈活的定制功能,例如加密存儲、解密讀取和操作攔截。
相信大家一定有所有收獲,快應用到自己的項目中吧!
作者:石小石Orz
原文地址:https://juejin.cn/post/7443658721600897035
該文章在 2024/12/11 11:25:51 編輯過