158 lines
4.7 KiB
TypeScript
158 lines
4.7 KiB
TypeScript
|
|
/**
|
|||
|
|
* 类型安全的 localStorage 封装,具备严格的 key 管理
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
// 1. 定义应用程序使用的存储键名(防止使用任意字符串作为 key)
|
|||
|
|
export enum AppStorageKey {
|
|||
|
|
PASSPORT_DATA = 'passportData',
|
|||
|
|
PASSPORT_TOKEN = 'passportToken',
|
|||
|
|
MENU_DATA = 'menuData', // 全部的菜单数据
|
|||
|
|
SLIDER_MENU = 'sliderMenu', // 侧边栏菜单数据
|
|||
|
|
USER_INFO = 'userInfo',
|
|||
|
|
TOKEN = 'token',
|
|||
|
|
MODE = 'mode', // 日间夜间模式
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 存储值类型定义(用于内部处理)
|
|||
|
|
type StorageValue<T> = {
|
|||
|
|
__data: T; // 实际存储的数据
|
|||
|
|
__expiry?: number; // 可选的过期时间戳
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 3. 主存储类
|
|||
|
|
class SafeStorage {
|
|||
|
|
/**
|
|||
|
|
* 存储数据(自动处理 JSON 序列化)
|
|||
|
|
* @param key 预定义的存储键
|
|||
|
|
* @param value 要存储的值(支持所有 JSON 安全类型)
|
|||
|
|
* @param ttl 可选的时间有效期(毫秒)
|
|||
|
|
*/
|
|||
|
|
static set<T>(key: AppStorageKey, value: T, ttl?: number): void {
|
|||
|
|
if (typeof window === "undefined") return;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 创建存储对象(包含数据值和过期时间)
|
|||
|
|
const storageValue: StorageValue<T> = {
|
|||
|
|
__data: value,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 设置过期时间(如果提供)
|
|||
|
|
if (ttl !== undefined && ttl > 0) {
|
|||
|
|
storageValue.__expiry = Date.now() + ttl;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 自动序列化并存储
|
|||
|
|
localStorage.setItem(key, JSON.stringify(storageValue));
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(`[存储] 保存失败 (key: ${key})`, error);
|
|||
|
|
this.handleStorageError(key, error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取数据(自动反序列化并检查过期)
|
|||
|
|
* @param key 预定义的存储键
|
|||
|
|
* @returns 存储的值或 null(如果不存在或过期)
|
|||
|
|
*/
|
|||
|
|
static get<T>(key: AppStorageKey): T | null {
|
|||
|
|
if (typeof window === "undefined") return null;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 获取原始数据
|
|||
|
|
const rawData = localStorage.getItem(key);
|
|||
|
|
if (!rawData) return null;
|
|||
|
|
|
|||
|
|
// 解析为内部存储结构
|
|||
|
|
const storageValue = JSON.parse(rawData) as StorageValue<T>;
|
|||
|
|
|
|||
|
|
// 检查是否过期
|
|||
|
|
if (this.isExpired(storageValue)) {
|
|||
|
|
this.remove(key);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 返回实际数据
|
|||
|
|
return storageValue.__data;
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(`[存储] 解析失败 (key: ${key})`, error);
|
|||
|
|
this.remove(key); // 移除无效数据
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 删除指定存储项
|
|||
|
|
* @param key 要删除的存储键
|
|||
|
|
*/
|
|||
|
|
static remove(key: AppStorageKey): void {
|
|||
|
|
if (typeof window === "undefined") return;
|
|||
|
|
localStorage.removeItem(key);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 清除应用相关的所有存储
|
|||
|
|
*/
|
|||
|
|
static clearAppStorage(): void {
|
|||
|
|
if (typeof window === "undefined") return;
|
|||
|
|
|
|||
|
|
// 遍历所有预定义的 key 进行删除
|
|||
|
|
Object.values(AppStorageKey).forEach(key => {
|
|||
|
|
localStorage.removeItem(key);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ----------------------------- 私有辅助方法 -----------------------------
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查存储值是否过期
|
|||
|
|
*/
|
|||
|
|
private static isExpired<T>(value: StorageValue<T>): boolean {
|
|||
|
|
return value.__expiry !== undefined && Date.now() > value.__expiry;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理存储错误(配额不足等)
|
|||
|
|
*/
|
|||
|
|
private static handleStorageError(key: AppStorageKey, error: unknown): void {
|
|||
|
|
// 处理存储空间不足错误
|
|||
|
|
if (error instanceof DOMException && error.name === 'QuotaExceededError') {
|
|||
|
|
console.warn('存储空间不足,尝试清理过期数据');
|
|||
|
|
this.clearExpiredItems();
|
|||
|
|
|
|||
|
|
// 尝试重新存储(最多尝试一次)
|
|||
|
|
try {
|
|||
|
|
const raw = localStorage.getItem(key);
|
|||
|
|
if (raw) {
|
|||
|
|
const value = JSON.parse(raw) as StorageValue<unknown>;
|
|||
|
|
if (!this.isExpired(value)) {
|
|||
|
|
localStorage.setItem(key, raw);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (retryError) {
|
|||
|
|
console.error('重试存储失败', retryError);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 清理所有过期的存储项
|
|||
|
|
*/
|
|||
|
|
private static clearExpiredItems(): void {
|
|||
|
|
Object.values(AppStorageKey).forEach(key => {
|
|||
|
|
try {
|
|||
|
|
const raw = localStorage.getItem(key);
|
|||
|
|
if (raw) {
|
|||
|
|
const value = JSON.parse(raw) as StorageValue<unknown>;
|
|||
|
|
if (this.isExpired(value)) {
|
|||
|
|
localStorage.removeItem(key);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
// 忽略无效数据
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default SafeStorage;
|