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:
- http://example.com/page1.html
- http://example.com/page2.html
而以下两个网址不共享同一个 localStorage,因为协议不同:
- http://example.com/page1.html
- 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 值后,在另一个页面中可以获取到这个值。