优雅使用本地存储

优雅使用本地存储

🤔 为什么需要本地存储?

在前端开发中,我们经常需要在浏览器中存储一些数据,比如:

  • 用户的偏好设置(如主题、语言)
  • 表单的部分填写内容(防止页面刷新丢失)
  • 应用的状态数据
  • 缓存的API数据

传统的Cookie存储有很多限制(大小、安全性等),而本地存储(LocalStorage和SessionStorage)提供了更方便、更安全的数据存储方式。但是,你真的会优雅地使用本地存储吗?

今天,我们就来学习如何优雅地使用本地存储,包括封装、类型安全、错误处理等方面的技巧!

💡 本地存储的基础使用

1. LocalStorage vs SessionStorage

特性LocalStorageSessionStorage
存储时长永久存储,除非手动删除会话期间有效,关闭标签页/浏览器后删除
存储大小约5MB约5MB
作用域同源的所有标签页/窗口共享仅当前标签页/窗口有效
数据类型仅支持字符串仅支持字符串

2. 基础操作

// 存储数据
localStorage.setItem('username', '张三');

// 获取数据
const username = localStorage.getItem('username');
console.log(username); // '张三'

// 删除数据
localStorage.removeItem('username');

// 清空所有数据
localStorage.clear();

// 获取所有键
const keys = Object.keys(localStorage);
console.log(keys);

🚀 优雅的本地存储封装

直接使用localStorage有很多缺点:

  • 只能存储字符串,需要手动序列化/反序列化
  • 没有类型安全
  • 没有错误处理
  • 键名容易冲突
  • 没有过期时间

让我们创建一个优雅的本地存储封装,解决这些问题!

1. 基础封装

/**
 * 本地存储工具类
 */
class StorageUtil {
  constructor(prefix = 'app_') {
    this.prefix = prefix;
    this.storage = localStorage;
  }

  /**
   * 生成带前缀的键名
   * @param {string} key - 原始键名
   * @returns {string} 带前缀的键名
   */
  getKey(key) {
    return `${this.prefix}${key}`;
  }

  /**
   * 存储数据
   * @param {string} key - 键名
   * @param {any} value - 要存储的值
   */
  set(key, value) {
    try {
      const serializedValue = JSON.stringify(value);
      this.storage.setItem(this.getKey(key), serializedValue);
    } catch (error) {
      console.error('存储数据失败:', error);
    }
  }

  /**
   * 获取数据
   * @param {string} key - 键名
   * @param {any} defaultValue - 默认值
   * @returns {any} 存储的值或默认值
   */
  get(key, defaultValue = null) {
    try {
      const serializedValue = this.storage.getItem(this.getKey(key));
      if (serializedValue === null) {
        return defaultValue;
      }
      return JSON.parse(serializedValue);
    } catch (error) {
      console.error('获取数据失败:', error);
      return defaultValue;
    }
  }

  /**
   * 删除数据
   * @param {string} key - 键名
   */
  remove(key) {
    try {
      this.storage.removeItem(this.getKey(key));
    } catch (error) {
      console.error('删除数据失败:', error);
    }
  }

  /**
   * 清空所有数据
   */
  clear() {
    try {
      // 只清空带前缀的数据
      const keys = Object.keys(this.storage);
      keys.forEach(key => {
        if (key.startsWith(this.prefix)) {
          this.storage.removeItem(key);
        }
      });
    } catch (error) {
      console.error('清空数据失败:', error);
    }
  }

  /**
   * 获取所有键名
   * @returns {string[]} 所有键名
   */
  keys() {
    try {
      const keys = Object.keys(this.storage);
      return keys
        .filter(key => key.startsWith(this.prefix))
        .map(key => key.replace(this.prefix, ''));
    } catch (error) {
      console.error('获取键名失败:', error);
      return [];
    }
  }
}

// 使用示例
const storage = new StorageUtil('my_app_');
storage.set('user', { id: 1, name: '张三' });
const user = storage.get('user');
console.log(user); // { id: 1, name: '张三' }

2. 支持过期时间的封装

/**
 * 带过期时间的本地存储工具类
 */
class ExpiringStorageUtil extends StorageUtil {
  constructor(prefix = 'app_', storage = localStorage) {
    super(prefix, storage);
  }

  /**
   * 存储带过期时间的数据
   * @param {string} key - 键名
   * @param {any} value - 要存储的值
   * @param {number} ttl - 过期时间(毫秒)
   */
  set(key, value, ttl = null) {
    const item = {
      value,
      expiry: ttl ? Date.now() + ttl : null
    };
    super.set(key, item);
  }

  /**
   * 获取数据(自动检查过期)
   * @param {string} key - 键名
   * @param {any} defaultValue - 默认值
   * @returns {any} 存储的值或默认值
   */
  get(key, defaultValue = null) {
    const item = super.get(key);
    if (item === null) {
      return defaultValue;
    }

    // 检查是否过期
    if (item.expiry && Date.now() > item.expiry) {
      super.remove(key);
      return defaultValue;
    }

    return item.value;
  }
}

// 使用示例
const expiringStorage = new ExpiringStorageUtil('my_app_');
// 存储2小时后过期的数据
expiringStorage.set('temp_data', '这是临时数据', 2 * 60 * 60 * 1000);
const tempData = expiringStorage.get('temp_data');
console.log(tempData); // '这是临时数据'

3. TypeScript版本(类型安全)

/**
 * 带类型安全的本地存储工具类
 */
class TypedStorageUtil<T extends Record<string, any>> {
  private prefix: string;
  private storage: Storage;

  constructor(prefix = 'app_', storage: Storage = localStorage) {
    this.prefix = prefix;
    this.storage = storage;
  }

  /**
   * 生成带前缀的键名
   */
  private getKey<K extends keyof T>(key: K): string {
    return `${this.prefix}${String(key)}`;
  }

  /**
   * 存储数据
   */
  set<K extends keyof T>(key: K, value: T[K], ttl?: number): void {
    try {
      const item = {
        value,
        expiry: ttl ? Date.now() + ttl : null
      };
      const serializedValue = JSON.stringify(item);
      this.storage.setItem(this.getKey(key), serializedValue);
    } catch (error) {
      console.error('存储数据失败:', error);
      throw error;
    }
  }

  /**
   * 获取数据
   */
  get<K extends keyof T>(key: K, defaultValue: T[K]): T[K] {
    try {
      const serializedValue = this.storage.getItem(this.getKey(key));
      if (serializedValue === null) {
        return defaultValue;
      }

      const item = JSON.parse(serializedValue);

      // 检查是否过期
      if (item.expiry && Date.now() > item.expiry) {
        this.storage.removeItem(this.getKey(key));
        return defaultValue;
      }

      return item.value;
    } catch (error) {
      console.error('获取数据失败:', error);
      return defaultValue;
    }
  }

  /**
   * 删除数据
   */
  remove<K extends keyof T>(key: K): void {
    try {
      this.storage.removeItem(this.getKey(key));
    } catch (error) {
      console.error('删除数据失败:', error);
      throw error;
    }
  }

  /**
   * 清空所有数据
   */
  clear(): void {
    try {
      const keys = Object.keys(this.storage);
      keys.forEach(key => {
        if (key.startsWith(this.prefix)) {
          this.storage.removeItem(key);
        }
      });
    } catch (error) {
      console.error('清空数据失败:', error);
      throw error;
    }
  }
}

// 使用示例
interface AppStorage {
  user: { id: number; name: string };
  theme: 'light' | 'dark';
  language: string;
  preferences: { notifications: boolean; autoSave: boolean };
}

const storage = new TypedStorageUtil<AppStorage>('my_app_');

// 类型安全的存储
storage.set('user', { id: 1, name: '张三' });
storage.set('theme', 'dark');
storage.set('language', 'zh-CN');

// 类型安全的获取
const user = storage.get('user', { id: 0, name: '' });
const theme = storage.get('theme', 'light');
const language = storage.get('language', 'en-US');

// 错误的类型会被TypeScript检查到
// storage.set('theme', 'red'); // 类型错误

🎯 React中的本地存储使用

在React中,我们可以创建自定义Hook来优雅地使用本地存储。

1. 基础自定义Hook

import { useState, useEffect } from 'react';

/**
 * 本地存储Hook
 * @param {string} key - 存储键名
 * @param {any} initialValue - 初始值
 * @param {Object} options - 选项
 * @returns {[any, function]} [存储的值, 设置值的函数]
 */
function useLocalStorage(key, initialValue, options = {}) {
  const { prefix = 'react_', storage = localStorage } = options;
  const prefixedKey = `${prefix}${key}`;

  // 从本地存储获取初始值
  const getStoredValue = () => {
    try {
      const item = storage.getItem(prefixedKey);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error('获取本地存储失败:', error);
      return initialValue;
    }
  };

  const [storedValue, setStoredValue] = useState(getStoredValue);

  // 设置值
  const setValue = (value) => {
    try {
      // 允许值是函数,类似于useState
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      storage.setItem(prefixedKey, JSON.stringify(valueToStore));
    } catch (error) {
      console.error('设置本地存储失败:', error);
    }
  };

  return [storedValue, setValue];
}

// 使用示例
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');

  const toggleTheme = () => {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
  };

  return (
    <div className={`app ${theme}`}>
      <button onClick={toggleTheme}>
        切换到{theme === 'light' ? '深色' : '浅色'}主题
      </button>
    </div>
  );
}

2. 支持过期时间的Hook

import { useState, useEffect } from 'react';

/**
 * 带过期时间的本地存储Hook
 * @param {string} key - 存储键名
 * @param {any} initialValue - 初始值
 * @param {number} ttl - 过期时间(毫秒)
 * @returns {[any, function]} [存储的值, 设置值的函数]
 */
function useLocalStorageWithExpiry(key, initialValue, ttl) {
  const prefixedKey = `react_${key}`;

  // 从本地存储获取初始值
  const getStoredValue = () => {
    try {
      const item = localStorage.getItem(prefixedKey);
      if (!item) {
        return initialValue;
      }

      const parsedItem = JSON.parse(item);

      // 检查是否过期
      if (parsedItem.expiry && Date.now() > parsedItem.expiry) {
        localStorage.removeItem(prefixedKey);
        return initialValue;
      }

      return parsedItem.value;
    } catch (error) {
      console.error('获取本地存储失败:', error);
      return initialValue;
    }
  };

  const [storedValue, setStoredValue] = useState(getStoredValue);

  // 设置值
  const setValue = (value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      const item = {
        value: valueToStore,
        expiry: ttl ? Date.now() + ttl : null
      };
      setStoredValue(valueToStore);
      localStorage.setItem(prefixedKey, JSON.stringify(item));
    } catch (error) {
      console.error('设置本地存储失败:', error);
    }
  };

  return [storedValue, setValue];
}

// 使用示例
function CacheExample() {
  const [cachedData, setCachedData] = useLocalStorageWithExpiry(
    'api_cache',
    null,
    5 * 60 * 1000 // 5分钟过期
  );

  // 模拟API请求
  const fetchData = async () => {
    if (cachedData) {
      console.log('使用缓存数据');
      return;
    }

    console.log('发起API请求');
    // 模拟API请求
    const data = await new Promise(resolve => {
      setTimeout(() => {
        resolve({ message: '这是API数据' });
      }, 1000);
    });

    setCachedData(data);
  };

  return (
    <div>
      <button onClick={fetchData}>
        获取数据
      </button>
      {cachedData && (
        <div>
          <h3>数据:</h3>
          <p>{JSON.stringify(cachedData)}</p>
        </div>
      )}
    </div>
  );
}

⚠️ 注意事项与最佳实践

1. 安全性考虑

  • 敏感数据不要存储在本地存储中:本地存储是明文存储的,不安全
  • 使用HTTPS:确保数据在传输过程中是加密的
  • 定期清理:不要永久存储不需要的数据

2. 性能优化

  • 不要存储大量数据:本地存储的大小有限(约5MB)
  • 避免频繁读写:频繁的读写会影响性能
  • 缓存数据:使用本地存储缓存不经常变化的数据

3. 兼容性

  • 本地存储在现代浏览器中都支持
  • 对于不支持的浏览器,可以提供polyfill
  • 检查浏览器是否支持本地存储
function isLocalStorageSupported() {
  try {
    const test = 'test';
    localStorage.setItem(test, test);
    localStorage.removeItem(test);
    return true;
  } catch (e) {
    return false;
  }
}

4. 错误处理

  • 始终使用try/catch包裹本地存储操作
  • 考虑存储配额满的情况
  • 提供降级方案

5. 命名规范

  • 使用前缀避免键名冲突
  • 使用有意义的键名
  • 保持键名的一致性

📝 总结

本地存储是前端开发中非常有用的功能,但直接使用原生API有很多缺点。通过封装,我们可以让本地存储变得更加优雅、安全、易用。

通过本文的介绍,我们学习了:

  1. 基础使用:LocalStorage和SessionStorage的区别和基础操作
  2. 优雅封装:解决了类型安全、错误处理、键名冲突等问题
  3. 高级功能:支持过期时间、类型安全等高级特性
  4. React集成:创建了自定义Hook来在React中优雅使用本地存储
  5. 最佳实践:安全性、性能、兼容性等方面的注意事项

希望这些小技巧对你有所帮助!下次使用本地存储时,不妨试试这些优雅的方法吧~✨


相关资源:

标签: #前端开发 #本地存储 #React #TypeScript #性能优化

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值