稀土掘金技术社区 2024年12月06日
如何优雅地重写 localStorage 、sessionStorage 方法?已封装,项目可直接使用!
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

前端开发中,localStorage 和 sessionStorage 是常用的数据存储方案,但有时无法满足业务需求,例如数据加密、访问日志记录等。本文介绍了一种通过代理重写原生 localStorage 和 sessionStorage 方法的技术方案,通过自定义钩子函数实现灵活的存储逻辑,例如数据加密、解密、拦截特定键值等。该方案还提供恢复原方法的机制,确保代码可控,不影响其他功能,并在 Vue 3 中提供了具体的应用示例,方便开发者理解和应用。

🤔 **重写原生方法:** 通过保存原生的 `setItem` 和 `getItem` 方法,并重写它们,在存储或读取过程中加入自定义逻辑,例如数据加密、校验或默认值填充。

🔄 **提供钩子函数:** 引入 `beforeSetItem` 和 `afterGetItem` 钩子函数,允许开发者自定义存储和读取过程中的逻辑,例如加密、解密、数据校验等,提升灵活性。

🛡️ **拦截特定键值:** 可以通过钩子函数拦截特定键值的存储或读取操作,例如阻止外部代码修改特定数据,实现系统数据保护。

🔄 **恢复原方法:** 提供 `unproxy` 方法,方便在特定场景下恢复原生的 localStorage 和 sessionStorage 方法,确保代码可控。

🚀 **Vue 3 应用示例:** 将代理方法封装成单例模式,并注入到 Vue 实例中,方便组件全局访问和使用。

原创 石小石Orz 2024-12-06 08:31 重庆

点击关注公众号,“技术干货“及时达!

点击关注公众号,“技术干货” 及时达!

需求简介

大家好,我是石小石!

在前端开发中,localStoragesessionStorage 是非常常见的数据存储解决方案。但在某些特殊场景下,原生的 localStoragesessionStorage 无法满足业务需求,例如:

在上面的场景中,我们通过重写原生的 localStoragesessionStorage 的方法,就可以实现这些特殊的需求。

技术方案

核心思路

要重写window上原生的方法,我们要先将原生的 setItemgetItem 方法保留下来,以便在需要时调用。然后,通过下面的伪代码重写方法,在存储或读取过程中加入自定义逻辑。

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);

这是因为originalGetItemoriginalSetItem 是从 localStoragesessionStorage 的原型方法保存下来的引用。如果直接调用 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');

总结

本文给大家介绍了通过代理 localStoragesessionStorage 实现自定义存储逻辑,满足特定业务需求、全局监控和数据保护等场景。

核心思路是重写原生的 setItemgetItem 方法,并通过钩子函数提供灵活的定制功能,例如加密存储、解密读取和操作拦截。

相信大家一定有所有收获,快应用到自己的项目中吧!

关注我,我是石小石!

点击关注公众号,“技术干货” 及时达!

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

localStorage sessionStorage 前端开发 代理 数据存储
相关文章