Web Storage:localStorage 与 sessionStorage

42 阅读2分钟

一、Web Storage 基础

1. localStorage 与 sessionStorage 对比

特性localStoragesessionStorage
生命周期永久存储,除非手动清除会话级别,标签页关闭即清除
作用域同源的所有标签页共享仅当前标签页可用
存储容量通常 5MB 左右通常 5MB 左右
数据格式仅支持字符串仅支持字符串
API 访问window.localStoragewindow.sessionStorage

2. 基本 API 使用

// 存储数据
localStorage.setItem('key', 'value');
sessionStorage.setItem('sessionKey', 'sessionValue');

// 读取数据
const value = localStorage.getItem('key');
const sessionValue = sessionStorage.getItem('sessionKey');

// 删除数据
localStorage.removeItem('key');
sessionStorage.removeItem('sessionKey');

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

// 获取键名
const keyName = localStorage.key(0); // 获取第一个键名

二、实际项目封装实践

在实际项目中,我们通常会对 Web Storage 进行封装,主要解决以下问题:

  • 自动序列化和反序列化 JSON 数据
  • 统一错误处理
  • 添加类型安全
  • 设置命名空间防止键名冲突
  • 添加过期时间功能

1. 基础封装实现

class WebStorage {
  constructor(private storage: Storage) {}
  
  // 设置存储项
  set<T>(key: string, value: T): void {
    try {
      const serialized = JSON.stringify(value);
      this.storage.setItem(key, serialized);
    } catch (error) {
      console.error(`Storage set error for key "${key}":`, error);
    }
  }
  
  // 获取存储项
  get<T>(key: string, defaultValue: T | null = null): T | null {
    try {
      const serialized = this.storage.getItem(key);
      return serialized ? JSON.parse(serialized) : defaultValue;
    } catch (error) {
      console.error(`Storage get error for key "${key}":`, error);
      return defaultValue;
    }
  }
  
  // 移除存储项
  remove(key: string): void {
    this.storage.removeItem(key);
  }
  
  // 清空存储
  clear(): void {
    this.storage.clear();
  }
}

// 创建实例
export const localStore = new WebStorage(localStorage);
export const sessionStore = new WebStorage(sessionStorage);

2. 带命名空间和过期时间的增强封装

interface StorageItem<T> {
  value: T;
  expires?: number; // 过期时间戳
}

class AdvancedStorage {
  constructor(
    private storage: Storage,
    private namespace: string
  ) {}
  
  private getKey(key: string): string {
    return `${this.namespace}:${key}`;
  }
  
  set<T>(key: string, value: T, ttl?: number): void {
    try {
      const storageKey = this.getKey(key);
      const item: StorageItem<T> = {
        value,
        expires: ttl ? Date.now() + ttl * 1000 : undefined
      };
      this.storage.setItem(storageKey, JSON.stringify(item));
    } catch (error) {
      console.error(`Storage set error:`, error);
    }
  }
  
  get<T>(key: string, defaultValue: T | null = null): T | null {
    try {
      const storageKey = this.getKey(key);
      const itemStr = this.storage.getItem(storageKey);
      
      if (!itemStr) return defaultValue;
      
      const item = JSON.parse(itemStr) as StorageItem<T>;
      
      // 检查是否过期
      if (item.expires && Date.now() > item.expires) {
        this.remove(key);
        return defaultValue;
      }
      
      return item.value;
    } catch (error) {
      console.error(`Storage get error:`, error);
      return defaultValue;
    }
  }
  
  remove(key: string): void {
    this.storage.removeItem(this.getKey(key));
  }
  
  clear(): void {
    Object.keys(this.storage)
      .filter(key => key.startsWith(`${this.namespace}:`))
      .forEach(key => this.storage.removeItem(key));
  }
}

// 使用示例
export const authStorage = new AdvancedStorage(localStorage, 'auth');
export const tempStorage = new AdvancedStorage(sessionStorage, 'temp');

三、实际项目应用场景

1. 用户认证信息存储

// 存储用户token (7天有效期)
authStorage.set('token', {
  accessToken: 'xxxxx',
  refreshToken: 'yyyyy'
}, 60 * 60 * 24 * 7);

// 获取token
const token = authStorage.get<{
  accessToken: string;
  refreshToken: string;
}>('token');

if (token) {
  // 使用token发起请求
  api.setAuthorization(token.accessToken);
}

2. 表单草稿保存

// 保存表单草稿
function saveDraft(formData: FormData) {
  sessionStore.set('formDraft', formData);
}

// 页面加载时恢复草稿
window.addEventListener('load', () => {
  const draft = sessionStore.get<FormData>('formDraft');
  if (draft) {
    restoreForm(draft);
  }
});

// 表单提交成功后清除草稿
function onSubmitSuccess() {
  sessionStore.remove('formDraft');
}

3. 应用偏好设置

// 用户主题偏好
const themeStore = new AdvancedStorage(localStorage, 'preference');

// 设置主题
function setTheme(theme: 'light' | 'dark') {
  themeStore.set('theme', theme);
  applyTheme(theme);
}

// 初始化时应用保存的主题
const savedTheme = themeStore.get<'light' | 'dark'>('theme', 'light');
applyTheme(savedTheme);

4. 购物车临时存储

const cartStorage = new AdvancedStorage(sessionStorage, 'cart');

// 添加商品到购物车
function addToCart(product: Product, quantity: number) {
  const currentCart = cartStorage.get<CartItem[]>('items') || [];
  const existingItem = currentCart.find(item => item.id === product.id);
  
  if (existingItem) {
    existingItem.quantity += quantity;
  } else {
    currentCart.push({
      id: product.id,
      name: product.name,
      price: product.price,
      quantity
    });
  }
  
  cartStorage.set('items', currentCart);
}

// 结账后清空购物车
function checkout() {
  // ...结账逻辑
  cartStorage.remove('items');
}

四、高级应用与优化

1. 存储加密

对于敏感信息,可以添加简单的加密:

import CryptoJS from 'crypto-js';

const SECRET_KEY = 'your-secret-key';

class SecureStorage extends AdvancedStorage {
  private encrypt(data: string): string {
    return CryptoJS.AES.encrypt(data, SECRET_KEY).toString();
  }
  
  private decrypt(cipherText: string): string {
    const bytes = CryptoJS.AES.decrypt(cipherText, SECRET_KEY);
    return bytes.toString(CryptoJS.enc.Utf8);
  }
  
  set<T>(key: string, value: T, ttl?: number): void {
    const item: StorageItem<T> = { value, expires: ttl ? Date.now() + ttl * 1000 : undefined };
    super.set(key, this.encrypt(JSON.stringify(item)));
  }
  
  get<T>(key: string, defaultValue: T | null = null): T | null {
    const encrypted = super.get<string>(key);
    if (!encrypted) return defaultValue;
    
    try {
      const decrypted = this.decrypt(encrypted);
      const item = JSON.parse(decrypted) as StorageItem<T>;
      
      if (item.expires && Date.now() > item.expires) {
        this.remove(key);
        return defaultValue;
      }
      
      return item.value;
    } catch {
      return defaultValue;
    }
  }
}

2. 存储事件监听

可以监听 storage 事件实现跨标签页通信:

// 封装存储事件监听
class StorageEventManager {
  private listeners: Record<string, Function[]> = {};
  
  constructor() {
    window.addEventListener('storage', this.handleStorageEvent);
  }
  
  private handleStorageEvent = (event: StorageEvent) => {
    if (!event.key || !this.listeners[event.key]) return;
    
    this.listeners[event.key].forEach(callback => {
      try {
        const newValue = event.newValue ? JSON.parse(event.newValue) : null;
        const oldValue = event.oldValue ? JSON.parse(event.oldValue) : null;
        callback(newValue, oldValue, event);
      } catch (error) {
        console.error('Storage event parse error:', error);
      }
    });
  };
  
  addListener(key: string, callback: Function) {
    if (!this.listeners[key]) {
      this.listeners[key] = [];
    }
    this.listeners[key].push(callback);
  }
  
  removeListener(key: string, callback: Function) {
    if (!this.listeners[key]) return;
    this.listeners[key] = this.listeners[key].filter(fn => fn !== callback);
  }
}

// 使用示例
export const storageEvent = new StorageEventManager();

// 在标签页A
authStorage.set('user', { name: 'Alice' });

// 在标签页B
storageEvent.addListener('auth:user', (newUser) => {
  console.log('User data updated:', newUser);
});

3. 存储容量检测

function testStorageQuota(storage: Storage): Promise<number> {
  return new Promise((resolve) => {
    const key = 'quota-test';
    let total = 0;
    const value = new Array(1024).fill('a').join(''); // 1KB
    
    const trySet = () => {
      try {
        storage.setItem(key, value + value);
        total += value.length;
        setTimeout(trySet, 0);
      } catch (e) {
        storage.removeItem(key);
        resolve(total);
      }
    };
    
    trySet();
  });
}

// 使用
testStorageQuota(localStorage).then(bytes => {
  console.log(`Available storage: ${(bytes / 1024 / 1024).toFixed(2)} MB`);
});

五、最佳实践

  1. 合理选择存储类型

    • 长期偏好设置 → localStorage
    • 会话临时数据 → sessionStorage
  2. 敏感信息处理

    • 避免存储密码、信用卡号等敏感信息
    • 必要时加密存储
  3. 数据清理策略

    • 设置合理的过期时间
    • 提供手动清理方法
    • 应用启动时清理过期数据
  4. 错误处理

    • 捕获可能的存储错误(如超出配额)
    • 提供降级方案
  5. 性能考虑

    • 避免存储大型数据(超过1MB)
    • 复杂数据考虑分块存储
  6. 类型安全

    • 使用TypeScript确保类型安全
    • 为存储的数据定义清晰接口