vue composables
useExpiredStorage
ts
// useExpiredStorage.ts
interface SetItemOptions {
expired?: number; // 过期的准确时间点,优先级比maxAge高 时间戳
maxAge?: number; // 从当前时间往后多长时间过期 比如 2 * 60 * 60 * 1000 表示2小时后过期
}
interface StorageData<T> {
value: T;
start: number; // 存储时间点 时间戳
expired: number; // 过期时间点 时间戳
}
interface ExpiredStorageOptions {
namespace: string; // 存储前缀
storageType?: 'session' | 'local'; // 存储类型,'session' 或 'local' 默认为 'local'
enableExpired?: boolean; // 是否开启过期时间,默认为 true
}
/**
* 带过期时间的storage组合式API
* @param namespace 自定义前缀,用于隔离不同项目,指定不同的命名空间
* @param storageType 存储类型,'session' 或 'local' 默认为 'local'
* @returns 存储操作方法
*/
export const useExpiredStorage = ({ namespace, storageType = 'local', enableExpired = true }: ExpiredStorageOptions) => {
const DEFAULT_EXPIRE_TIME = 2 * 60 * 60 * 1000; // 默认过期时间2小时
const storage = storageType === 'local' ? window.localStorage : window.sessionStorage;
const prefix = `${namespace}:`;
console.log(namespace, storageType, enableExpired, prefix);
/**
* 检查存储配额是否可用
* @returns 存储是否可用
*/
const checkStorageQuota = (): boolean => {
try {
const testKey = `${prefix}__test__`;
storage.setItem(testKey, 'test');
storage.removeItem(testKey);
return true;
} catch {
return false;
}
};
/**
* 解析存储数据
* @param data 存储的JSON字符串
* @returns 解析后的数据对象或null
*/
const parseStorageData = <T>(data: string): StorageData<T> | null => {
try {
return JSON.parse(data) as StorageData<T>;
} catch {
return null;
}
};
/**
* 验证数据结构是否有效,检查是否为StorageData<T>
* @param data 任意数据
* @returns 是否为有效的存储数据结构, 包含value, start, expired属性
*/
const isValidStorageData = (data: any): data is StorageData<any> => {
return data && typeof data === 'object' && 'value' in data && 'start' in data && 'expired' in data && typeof data.expired === 'number';
};
/**
* 设置数据
* @param key 键名
* @param value 值
* @param options 选项
* @returns 是否设置成功
*/
const setItem = <T>(key: string, value: T, options?: SetItemOptions): boolean => {
// 校验key是否为空
if (!key || key.trim() === '') {
console.warn('expiredStorage: Key cannot be empty');
return false;
}
// 检查存储配额是否可用
if (!checkStorageQuota()) {
console.warn('expiredStorage: Storage quota exceeded');
return false;
}
try {
const storageKey = `${prefix}${key}`;
// 根据enableExpired决定存储格式
if (enableExpired) {
const now = Date.now();
let expired: number;
// 处理过期时间字段
if (options?.expired) {
expired = options.expired;
} else if (options?.maxAge) {
expired = now + options.maxAge;
} else {
expired = now + DEFAULT_EXPIRE_TIME;
}
const data: StorageData<T> = {
value,
start: now,
expired,
};
storage.setItem(storageKey, JSON.stringify(data));
} else {
// 不启用过期时间时,直接存储值
storage.setItem(storageKey, JSON.stringify(value));
}
return true;
} catch (error) {
console.error('Failed to set item in expiredStorage:', error);
return false;
}
};
/**
* 检查key是否存在且未过期
* @param key 键名
* @returns 是否存在且未过期
*/
const hasItem = (key: string): boolean => {
try {
const storageKey = `${prefix}${key}`;
const result = storage.getItem(storageKey);
// 无数据
if (!result) {
removeItem(key);
return false;
}
// 未启用过期时间时,直接返回true
if (!enableExpired) {
return true;
}
// 没有数据/数据格式无效则认为不存在
const data = parseStorageData<any>(result);
if (!data || !isValidStorageData(data)) {
removeItem(key);
return false;
}
const notExpired = Date.now() <= data.expired;
// 已过期则清除并返回false
if (!notExpired) {
removeItem(key);
}
return notExpired;
} catch {
return false;
}
};
/**
* 获取数据
* @param key 键名
* @returns 值或null
*/
const getItem = <T>(key: string): T | null => {
try {
// 获取时若不存在或已过期
if (!hasItem(key)) {
return null;
}
const result = storage.getItem(`${prefix}${key}`) as string;
// 如果不启用过期时间,直接解析并返回值
if (!enableExpired) {
try {
return JSON.parse(result) as T;
} catch {
// 解析失败时删除无效数据
removeItem(key);
return null;
}
}
return parseStorageData<T>(result)?.value as T | null;
} catch (error) {
console.error('Failed to get item from expiredStorage:', error);
removeItem(key); // 解析错误时删除无效数据
return null;
}
};
/**
* 移除指定key
* @param key 键名
*/
const removeItem = (key: string): void => {
try {
storage.removeItem(`${prefix}${key}`);
} catch (error) {
console.error('Failed to remove item from expiredStorage:', error);
}
};
/**
* 获取所有未过期的key
* @returns key数组(含前缀)
*/
const getAllKeys = (): string[] => {
const keys: string[] = [];
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
if (key?.startsWith(prefix)) {
const value = getItem(key.replace(prefix, ''));
if (value) {
keys.push(key);
}
}
}
return keys;
};
/**
* 清除所有带前缀的数据
* @returns 删除的完整key数组(含前缀)
*/
const clear = (): string[] => {
const keysToRemove: string[] = [];
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
if (key?.startsWith(prefix)) {
keysToRemove.push(key);
}
}
keysToRemove.forEach((key) => {
storage.removeItem(key);
});
return keysToRemove;
};
/**
* 清除当前命名空间所有过期的数据
* @returns 删除的完整key数组(含前缀)
*/
const clearExpired = (): string[] => {
const keysToRemove: string[] = [];
// 收集所有需要删除的key
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
if (key && key.startsWith(prefix)) {
// 判断存在时同时已经删除了过期数据
if (!hasItem(key.replace(prefix, ''))) {
keysToRemove.push(key);
}
}
}
return keysToRemove;
};
/**
* 清除所有存储,包括非命名空间的数据(谨慎使用)
*/
const clearStorage = (): void => {
storage.clear();
};
// 返回所有可用的方法和状态
return {
setItem,
getItem,
hasItem,
removeItem,
clear,
clearExpired,
clearStorage,
getAllKeys,
};
};