需求简介
在前端开发中,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 编辑过