Storage 工具如何封装(前缀、加密、过期时间等)及localStorage的基础介绍

​​​​​​​

Storage 工具类封装

该工具函数设计

  • 采用工厂方法+闭包设计模式,不直接实例化类,而是根据传入的参数来配置和返回一个 SmartStorage 的实例。
  • 支持带前缀的键:通过 prefixKey 参数可以为存储的键名添加一个前缀,默认为空字符串。这个功能可以帮助避免键名冲突,特别是当在同一个域下的不同应用或组件中使用同一种存储方式时。
  • 支持过期时间:在存储数据时,可以为每项数据设置一个过期时间(单位为秒),存储的数据结构中会包括实际的值、存储时间戳以及过期时间戳。在读取数据时,会检查数据是否过期,如果已经过期,则自动删除
  • 支持加密存储:存储数据时根据参数配置可先进行加密,读取数据时再解密,加密使用的 crypto 模块
  • 错误处理:在读取数据时,如果解密过程出错或数据格式不正确,会捕获异常并返回默认值,这提高了程序的健壮性。
  • 支持常用的 api(set get remove clear)
  • TypeScript 实现

接下来是代码实现:在未进行代码实现前可以基于上面的设计自己实现一下,然后对照下我的代码实现

/***
 * title: storage.ts
 * Desc: 对存储的简单封装
 */
// 加密库
import CryptoJS from "crypto-js";
import { projectPrefix, expireTimes } from "../variable";
 
// 十六位十六进制数作为密钥
const SECRET_KEY = CryptoJS.enc.Utf8.parse("3333e66666111111");
// 十六位十六进制数作为密钥偏移量
const SECRET_IV = CryptoJS.enc.Utf8.parse("3333bb2222111111");
 
// 类型 window.localStorage,window.sessionStorage,
type Config = {
  type: any;
  prefix: string;
  expire: any;
  isEncrypt: Boolean;
};
const config: Config = {
  type: "localStorage", // 本地存储类型 sessionStorage
  prefix: projectPrefix, // 名称前缀 建议:项目名 + 项目版本
  expire: expireTimes, //过期时间 单位:秒
  isEncrypt: true, // 默认加密 为了调试方便, 开发过程中可以不加密
};
 
// 判断是否支持 Storage
export const isSupportStorage = () => {
  return typeof Storage !== "undefined" ? true : false;
};
 
// 设置 setStorage
export const setStorage = (key: string, value: any, expire = 0) => {
  if (value === "" || value === null || value === undefined) {
    value = null;
  }
 
  if (isNaN(expire) || expire < 0) throw new Error("Expire must be a number");
 
  expire = expire ? expire : config.expire;
  let data = {
    value: value, // 存储值
    time: Date.now(), //存值时间戳
    expire: expire, // 过期时间
  };
 
  console.log("shezhi ", data);
 
  const encryptString = config.isEncrypt
    ? encrypt(JSON.stringify(data))
    : JSON.stringify(data);
 
  (window[config.type] as any).setItem(autoAddPrefix(key), encryptString);
};
 
// 获取 getStorage
export const getStorage = (key: string) => {
  key = autoAddPrefix(key);
  // key 不存在判断
  if (
    !(window[config.type] as any).getItem(key) ||
    JSON.stringify((window[config.type] as any).getItem(key)) === "null"
  ) {
    return null;
  }
 
  // 优化 持续使用中续期
  const storage = config.isEncrypt
    ? JSON.parse(decrypt((window[config.type] as any).getItem(key)))
    : JSON.parse((window[config.type] as any).getItem(key));
 
  let nowTime = Date.now();
 
  // 过期删除
 
  let setExpire = (storage.expire || config.expire) * 1000,
    expDiff = nowTime - storage.time;
  console.log("设置时间", setExpire, expDiff);
  if (setExpire < expDiff) {
    removeStorage(key);
    return null;
  } else {
    // 未过期期间被调用 则自动续期 进行保活
    setStorage(autoRemovePrefix(key), storage.value, storage.expire);
    return storage.value;
  }
};
 
// 是否存在 hasStorage
export const hasStorage = (key: string) => {
  key = autoAddPrefix(key);
  let arr = getStorageAll().filter((item) => {
    return item.key === key;
  });
  return arr.length ? true : false;
};
 
// 获取所有key
export const getStorageKeys = () => {
  let items = getStorageAll();
  let keys = [];
  for (let index = 0; index < items.length; index++) {
    keys.push(items[index].key);
  }
  return keys;
};
 
// 获取全部 getAllStorage
export const getStorageAll = () => {
  let len = window[config.type].length; // 获取长度
  let arr = new Array(); // 定义数据集
  for (let i = 0; i < len; i++) {
    // 获取key 索引从0开始
    let getKey = (window[config.type] as any).key(i);
    // 获取key对应的值
    let getVal = (window[config.type] as any).getItem(getKey);
    // 放进数组
    arr[i] = { key: getKey, val: getVal };
  }
  return arr;
};
 
// 删除 removeStorage
export const removeStorage = (key: string) => {
  (window[config.type] as any).removeItem(autoAddPrefix(key));
};
 
// 清空 clearStorage
export const clearStorage = () => {
  (window[config.type] as any).clear();
};
 
// 名称前自动添加前缀
const autoAddPrefix = (key: string) => {
  const prefix = config.prefix ? config.prefix + "_" : "";
  return prefix + key;
};
 
// 移除已添加的前缀
const autoRemovePrefix = (key: string) => {
  const len: any = config.prefix ? config.prefix.length + 1 : "";
  return key.substr(len);
  // const prefix = config.prefix ? config.prefix + '_' : '';
  // return  prefix + key;
};
 
/**
 * 加密方法
 * @param data
 * @returns {string}
 */
const encrypt = (data: any) => {
  if (typeof data === "object") {
    try {
      data = JSON.stringify(data);
    } catch (error) {
      console.log("encrypt error:", error);
    }
  }
  const dataHex = CryptoJS.enc.Utf8.parse(data);
  const encrypted = CryptoJS.AES.encrypt(dataHex, SECRET_KEY, {
    iv: SECRET_IV,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7,
  });
  return encrypted.ciphertext.toString();
};
 
/**
 * 解密方法
 * @param data
 * @returns {string}
 */
const decrypt = (data: any) => {
  const encryptedHexStr = CryptoJS.enc.Hex.parse(data);
  const str = CryptoJS.enc.Base64.stringify(encryptedHexStr);
  const decrypt = CryptoJS.AES.decrypt(str, SECRET_KEY, {
    iv: SECRET_IV,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7,
  });
  const decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
  return decryptedStr.toString();
};

在组件中使用:

<template>
  <div>
    layout
    <el-button type="primary" @click="getLocal">get</el-button>
    <el-button type="primary" @click="setLocal">set</el-button>
  </div>
</template>
 
<script setup lang="ts">
import { setStorage, getStorage, getStorageAll,removeStorage, clearStorage } from "../../utils/modules/storage";
 
 
let setLocal = (): void => {
  let testLocal = "token123123123";
  setStorage("testLocal", testLocal);
};
 
let getLocal = (): void => {
  let localVal = getStorage("testLocal");
  let all = getStorageAll()
  console.log("测试存储", localVal,all);
};
</script>
 
<style scoped lang="scss">
 
</style>
localStorlocalStorage 存储大小

localStorage 的存储大小因浏览器而异,但通常有以下限制:

  • 大多数浏览器提供了5MB左右的存储空间,但是单位是字符串的长度值, 或者 utf-16 的编码单元,也可以说是 10M 字节空间。
  • localStorage 的 key 键也是占存储空间的。
  • localStorage 如何统计已使用空间
  • 在移动设备上,iOS 和某些版本的 Android 浏览器可能会对 localStorage 的大小有所限制,甚至完全禁用它。

要检查 localStorage 的实际存储限制,可以使用以下代码片段尝试使用存储空间:

function checkLocalStorageSpace() {
    var testKey = 'storageTest',
        max = 2 * 1024 * 1024, // 2MB
        i = 0,
        value = '';
 
    // 生成一个足够大的字符串
    for (i = 0; i < max; i++) {
        value += 'a';
    }
 
    // 尝试存储这个大字符串
    localStorage.setItem(testKey, value);
 
    // 检查是否能成功存储,如果不能,则减半大小继续测试
    try {
        localStorage.setItem(testKey, value);
        max *= 2;
    } catch (e) {
        max /= 2;
    }
 
    localStorage.removeItem(testKey);
 
    console.log('Estimated localStorage limit: ' + max + ' bytes');
}
 
checkLocalStorageSpace();

关于工具函数的封装:

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如何监听

localStorage 本身并没有提供监听变化的API,但可以通过轮询或者MutationObserver来实现监听。

轮询方法:

function checkLocalStorage() {
    var currentCart = localStorage.getItem("cart");
    if (currentCart !== cart) { // 假设cart是之前存储的值
        // 这里处理变化逻辑
        console.log("localStorage changed!");
        cart = currentCart;
    }
}
 
// 设置一个间隔时间轮询
setInterval(checkLocalStorage, 500);

localStorage如何监听

  • localStorage 本身并没有提供监听变化的API,但可以通过轮询或者MutationObserver来实现监听。

轮询方法:

function checkLocalStorage() {
    var currentCart = localStorage.getItem("cart");
    if (currentCart !== cart) { // 假设cart是之前存储的值
        // 这里处理变化逻辑
        console.log("localStorage changed!");
        cart = currentCart;
    }
}
 
// 设置一个间隔时间轮询
setInterval(checkLocalStorage, 500);

MutationObserver 方法:

var cart = localStorage.getItem("cart");
 
var observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
        if (mutation.type === 'update' && mutation.target === localStorage) {
            for (var i = 0; i < mutation.addedNodes.length; i++) {
                var node = mutation.addedNodes[i];
                if (node.key === "cart") {
                    // 这里处理变化逻辑
                    console.log("localStorage changed!");
                }
            }
        }
    });
});
 
var config = { attributes: true, childList: true, subtree:true };
 
observer.observe(localStorage, config);

需要注意的是,MutationObserver 的兼容性较好,但它不会监听存储空间的变化,只能监听对localStorage的API调用。轮询方法可以一定程度上实现这个功能,但是会对性能有影响,应该谨慎使用。

  • 原生 api 监听
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);
        }
localStorage 同源

只有来自同一个源的网页才能访问相同的 localStorage 对应 key 的数据,这也是前面工具类封装,这个参数 prefixKey 的作用,同源项目可以加一个唯一 key,保证同源下的 localStorage 不冲突。

同源窗口通信

localStorage 是一种客户端存储机制,用于在用户的浏览器中保存键值对。localStorage 遵循同源策略,也就是说,两个网页要能访问相同的 localStorage 数据,它们必须有相同的协议(如 http),相同的端口(如 80 或 443),相同的主机(比如 example.com)。

  • 每个窗口都需要有一个独一无二的标识符(ID),以便在众多窗口中准确识别和管理它们。
  • 为了避免同一个消息被重复处理,必须有机制确保消息的唯一性。
  • 还需要确保只有那些真正需要接收特定消息的窗口才会收到通知,这就要求消息分发机制能够有效地过滤掉不相关的窗口。
  • 考虑到窗口可能会因为各种原因关闭或变得不响应,引入窗口的“心跳”机制来监控它们的活跃状态变得尤为重要。
  • 当涉及到需要从多个窗口中选举出一个主窗口来协调操作时,主窗口的选举机制也是不可或缺的一环。

例如,以下两个网址共享同一个 localStorage:

  1. http://example.com/page1.html
  2. http://example.com/page2.html

而以下两个网址不共享同一个 localStorage,因为协议不同:

  1. http://example.com/page1.html
  2. https://example.com/page2.html

以下是一个简单的例子,演示如何在同一源的页面间共享 localStorage 数据:

<!-- page1.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Page 1</title>
    <script>
        // 设置 localStorage 数据
        localStorage.setItem('key', 'value');
 
        // 获取 localStorage 数据
        function getLocalStorageValue() {
            var value = localStorage.getItem('key');
            console.log('localStorage value:', value);
        }
    </script>
</head>
<body>
    <button onclick="getLocalStorageValue()">Get localStorage value</button>
</body>
</html>
<!-- page2.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Page 2</title>
    <script>
        // 获取 page1.html 设置的 localStorage 数据
        function getLocalStorageValue() {
            var value = localStorage.getItem('key');
            console.log('localStorage value:', value);
        }
    </script>
</head>
<body>
    <button onclick="getLocalStorageValue()">Get localStorage value</button>
</body>
</html>

在上述例子中,page1.html 和 page2.html 通过 localStorage 共享数据。当在一个页面中设置 localStorage 值后,在另一个页面中可以获取到这个值。

  • 20
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
封装localstorage,使其支持设置过期时间可以通过以下步骤实现: 1. 创建一个自定义的封装函数,例如setLocalStorageWithExpire(key, value, expire),该函数接受三个参数:key表示存储的键名,value表示存储的值,expire表示过期时间。 2. 在该函数内部,首先计算存储数据的过期时间,可以使用Date对象的getTime()方法获取当前时间时间戳,然后根据过期时间计算出具体的过期时间戳。 3. 接下来使用localStorage.setItem()方法将键值对存储到本地存储中,可以使用JSON对象的方法将value转换为字符串再进行存储。 4. 在存储键值对的同时,还需要将过期时间存储起来,可以使用localStorage.setItem()方法将过期时间戳存储到另一个键名中,例如expire_key,作为过期时间的标志。 5. 在需要获取存储数据时,创建一个新的函数getLocalStorageWithExpire(key),该函数接受一个参数key表示需要获取的键名。 6. 在该函数内部,首先使用localStorage.getItem()方法获取键值对的值,然后使用JSON对象的parse()方法将字符串转换为对象格式,得到存储的值。 7. 紧接着,使用localStorage.getItem()方法获取过期时间时间戳,然后与当前时间戳进行比较,若过期时间大于当前时间,则说明数据还未过期,返回存储的值。 8. 如果过期时间小于等于当前时间,则说明数据已过期,可以使用localStorage.removeItem()方法将键值对和过期时间从本地存储中移除。 通过以上步骤,我们可以对localStorage进行封装,使其支持设置过期时间,方便在存储数据时加入过期时间的管理,从而实现更加灵活和可控的本地存储机制。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卡夫卡的小熊猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值