如何优雅地重写 localStorage 、sessionStorage 方法?已封装,项目可直接使用!

需求简介

大家好,我是石小石!

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

  • 业务定制化需求:需要在存储和获取某些特定键时加入逻辑,比如数据加密、校验或默认值填充。
  • 全局监控:希望对存储和读取操作进行监控,例如记录关键数据的访问日志或统计操作频率。
  • 系统数据保护:防止外部代码对特定键值的误改动。

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

技术方案

核心思路

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

js 复制代码
const _setItem = localStorage.setItem;
localStorage.setItem = function (...args) {
    // 自定义逻辑....
    // 最终调用_setItem
};

最后,我们也可以提供恢复原方法的机制,确保代码可控,不影响其他功能。

由于我们的重写的是window上的方法,因此,重写的时机一定要尽可能的早。比如,我们使用的是vue项目,我们就应该在vue实例创建前,实现原生方法的重写:

js 复制代码
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);

// 挂载应用

代理存储方法

初步实现:简单拦截

我们可以实现一个简单的代理,针对特定键值在存储和读取时加入逻辑(根据业务而定)。例如:

js 复制代码
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?

js 复制代码
originalSetItem.call(this, key, value);

这是因为originalGetItemoriginalSetItem 是从 localStoragesessionStorage 的原型方法保存下来的引用。如果直接调用 originalSetItem(key, value)originalGetItem('origin_system'),它们的上下文(this)会丢失。

js 复制代码
const setItem = localStorage.setItem;
setItem('key', 'value'); // 会报错:Cannot read properties of undefined

这是因为 setItem 的上下文丢失,它不再知道自己属于 localStorage

提供灵活的配置能力

为了应对更多场景需求,我们可以引入配置选项,让代理逻辑更加灵活,比如,加入自定义钩子函数,允许用户自定义重写的逻辑。

js 复制代码
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:加密存储数据

js 复制代码
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:监控存储操作

记录存储和读取行为:

js 复制代码
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:拦截特定键值

阻止某些特定键的存储或读取:

js 复制代码
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页面时,我们可能需要终止代理。因此,我们需要提供一个终止代理的方法。

js 复制代码
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;
  
}

使用示例

js 复制代码
// 代理 localStorage
const unproxy = proxyStorage(localStorage, config);

// 使用 localStorage
localStorage.setItem('key', '12345'); // 被拦截

// 恢复原始方法
unproxyLocalStorage();

整合后的最终代码

我们可以将这个方法直接封装成一个类,方便调用

js 复制代码
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

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 实例中,便于全局访问:

js 复制代码
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 方法,并通过钩子函数提供灵活的定制功能,例如加密存储、解密读取和操作拦截。

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

关注我,我是石小石!

相关推荐
Nan_Shu_61418 分钟前
学习: Threejs (2)
前端·javascript·学习
G_G#26 分钟前
纯前端js插件实现同一浏览器控制只允许打开一个标签,处理session变更问题
前端·javascript·浏览器标签页通信·只允许一个标签页
@大迁世界41 分钟前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路1 小时前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug1 小时前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu121381 小时前
React面向组件编程
开发语言·前端·javascript
持续升级打怪中1 小时前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路1 小时前
GDAL 实现矢量合并
前端
hxjhnct1 小时前
React useContext的缺陷
前端·react.js·前端框架
冰暮流星2 小时前
javascript逻辑运算符
开发语言·javascript·ecmascript