Skip to content

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,
  };
};
既来之,则安之。