一、Storage 接口概述

Storage 接口是浏览器 Web Storage API 的核心,为客户端提供了持久化存储键值对数据的能力。

1.1 基本概念

javascript

// Storage 接口定义了两个主要实现:
// 1. localStorage - 永久存储,除非手动清除
// 2. sessionStorage - 会话存储,标签页关闭时清除

// 类型声明(TypeScript)
interface Storage {
  readonly length: number;
  clear(): void;
  getItem(key: string): string | null;
  key(index: number): string | null;
  removeItem(key: string): void;
  setItem(key: string, value: string): void;
  [name: string]: any;
}

1.2 基本特性

特性说明
同源策略仅在同源(协议、域名、端口相同)页面间共享
存储类型只存储字符串,其他类型需序列化
存储容量通常 5-10MB(因浏览器而异)
同步操作所有操作都是同步的,可能阻塞主线程
无过期时间localStorage 永久存储,sessionStorage 会话结束清除

二、localStorage 详解

2.1 基本操作

javascript

// 1. 设置数据
localStorage.setItem('username', '张三');
localStorage.setItem('age', '25');

// 2. 获取数据
const username = localStorage.getItem('username'); // "张三"
const age = localStorage.getItem('age'); // "25"

// 3. 删除单个数据
localStorage.removeItem('age');

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

// 5. 获取键名和长度
const keyName = localStorage.key(0); // 获取第一个键名
const storageLength = localStorage.length; // 存储项数量

2.2 存储复杂数据类型

javascript

// 存储对象
const user = {
  name: '李四',
  age: 30,
  email: 'lisi@example.com'
};

// 序列化存储
localStorage.setItem('user', JSON.stringify(user));

// 读取并反序列化
const storedUser = JSON.parse(localStorage.getItem('user') || '{}');
console.log(storedUser.name); // "李四"

// 存储数组
const todos = ['学习', '工作', '休息'];
localStorage.setItem('todos', JSON.stringify(todos));

const storedTodos = JSON.parse(localStorage.getItem('todos') || '[]');

2.3 存储限制和错误处理

javascript

// 检查存储空间
function checkStorageSpace() {
  try {
    const testKey = 'test';
    let data = '';
    // 生成大约 1MB 的数据
    for (let i = 0; i < 1024 * 1024; i++) {
      data += 'a';
    }
    
    localStorage.setItem(testKey, data);
    localStorage.removeItem(testKey);
    return true;
  } catch (e) {
    if (e.code === 22 || e.code === 1014) {
      console.warn('存储空间已满');
      return false;
    }
  }
}

// 安全存储(带错误处理)
function safeSetItem(key, value) {
  try {
    localStorage.setItem(key, value);
    return true;
  } catch (error) {
    console.error('存储失败:', error);
    
    // 尝试清理旧数据
    if (error.name === 'QuotaExceededError') {
      // 清理策略示例:删除最旧的数据
      const keys = Object.keys(localStorage);
      if (keys.length > 0) {
        localStorage.removeItem(keys[0]);
        // 重试
        return safeSetItem(key, value);
      }
    }
    return false;
  }
}

三、sessionStorage 详解

3.1 与 localStorage 的区别

javascript

// 生命周期对比
localStorage.setItem('persistent', '我会一直存在');
sessionStorage.setItem('temporary', '标签页关闭我就消失');

// 作用域对比
// localStorage: 同源的所有标签页共享
// sessionStorage: 仅当前标签页有效

// 测试标签页间通信
// 页面A
localStorage.setItem('shared', '所有标签页都能看到我');
sessionStorage.setItem('private', '只有这个标签页能看到我');

// 页面B(同源)
console.log(localStorage.getItem('shared')); // "所有标签页都能看到我"
console.log(sessionStorage.getItem('private')); // null

3.2 典型应用场景

javascript

// 1. 表单数据暂存
class FormAutoSave {
  constructor(formId, storageKey = 'formData') {
    this.form = document.getElementById(formId);
    this.storageKey = storageKey;
    this.init();
  }
  
  init() {
    // 恢复数据
    this.restoreData();
    
    // 监听输入变化
    this.form.addEventListener('input', (e) => {
      this.saveData();
    });
    
    // 表单提交时清除
    this.form.addEventListener('submit', () => {
      sessionStorage.removeItem(this.storageKey);
    });
  }
  
  saveData() {
    const formData = new FormData(this.form);
    const data = {};
    for (let [key, value] of formData.entries()) {
      data[key] = value;
    }
    sessionStorage.setItem(this.storageKey, JSON.stringify(data));
  }
  
  restoreData() {
    const saved = sessionStorage.getItem(this.storageKey);
    if (saved) {
      const data = JSON.parse(saved);
      Object.keys(data).forEach(key => {
        const input = this.form.querySelector(`[name="${key}"]`);
        if (input) {
          input.value = data[key];
        }
      });
    }
  }
}

四、Storage 事件机制

4.1 storage 事件详解

javascript

// 监听 storage 事件
window.addEventListener('storage', (event) => {
  console.log('Storage 变化:', {
    key: event.key,           // 变化的键名
    oldValue: event.oldValue, // 旧值
    newValue: event.newValue, // 新值
    url: event.url,           // 修改来源的URL
    storageArea: event.storageArea // 影响的存储区域
  });
});

// 注意:storage 事件只在其他同源窗口修改 Storage 时触发
// 当前窗口的修改不会触发自己的 storage 事件

4.2 跨标签页通信方案

javascript

// 标签页间通信管理器
class TabCommunication {
  constructor(channel = 'tab-comm') {
    this.channel = channel;
    this.listeners = new Map();
    this.init();
  }
  
  init() {
    // 监听 storage 事件
    window.addEventListener('storage', (event) => {
      if (event.key === this.channel && event.newValue) {
        try {
          const data = JSON.parse(event.newValue);
          this.notifyListeners(data);
        } catch (e) {
          console.error('解析消息失败:', e);
        }
      }
    });
    
    // 监听页面卸载
    window.addEventListener('beforeunload', () => {
      this.cleanup();
    });
  }
  
  send(message) {
    const data = {
      ...message,
      timestamp: Date.now(),
      source: window.location.href
    };
    
    localStorage.setItem(this.channel, JSON.stringify(data));
    
    // 立即移除,避免重复触发
    setTimeout(() => {
      localStorage.removeItem(this.channel);
    }, 0);
  }
  
  on(type, callback) {
    if (!this.listeners.has(type)) {
      this.listeners.set(type, []);
    }
    this.listeners.get(type).push(callback);
  }
  
  notifyListeners(data) {
    const callbacks = this.listeners.get(data.type) || [];
    callbacks.forEach(callback => callback(data));
  }
  
  cleanup() {
    localStorage.removeItem(this.channel);
  }
}

// 使用示例
const comm = new TabCommunication();

// 发送消息
comm.send({
  type: 'user-updated',
  userId: '123',
  action: 'login'
});

// 接收消息
comm.on('user-updated', (data) => {
  console.log('收到更新:', data);
});

五、高级应用与最佳实践

5.1 Storage 封装类

javascript

class StorageManager {
  constructor(storage = localStorage, prefix = 'app_') {
    this.storage = storage;
    this.prefix = prefix;
  }
  
  // 生成带前缀的键名
  getKey(key) {
    return `${this.prefix}${key}`;
  }
  
  // 设置带过期时间的数据
  setWithExpiry(key, value, ttl = 3600000) { // 默认1小时
    const item = {
      value: value,
      expiry: Date.now() + ttl
    };
    this.storage.setItem(this.getKey(key), JSON.stringify(item));
  }
  
  // 获取数据,自动处理过期
  getWithExpiry(key) {
    const itemStr = this.storage.getItem(this.getKey(key));
    
    if (!itemStr) return null;
    
    try {
      const item = JSON.parse(itemStr);
      
      // 检查是否过期
      if (Date.now() > item.expiry) {
        this.remove(key);
        return null;
      }
      
      return item.value;
    } catch (e) {
      return null;
    }
  }
  
  // 批量操作
  setMany(items) {
    Object.entries(items).forEach(([key, value]) => {
      this.set(key, value);
    });
  }
  
  getMany(keys) {
    const result = {};
    keys.forEach(key => {
      result[key] = this.get(key);
    });
    return result;
  }
  
  // 原生方法封装
  set(key, value) {
    const strValue = typeof value === 'string' ? value : JSON.stringify(value);
    this.storage.setItem(this.getKey(key), strValue);
  }
  
  get(key) {
    const value = this.storage.getItem(this.getKey(key));
    try {
      return JSON.parse(value);
    } catch {
      return value;
    }
  }
  
  remove(key) {
    this.storage.removeItem(this.getKey(key));
  }
  
  clear() {
    const prefix = this.prefix;
    const keysToRemove = [];
    
    for (let i = 0; i < this.storage.length; i++) {
      const key = this.storage.key(i);
      if (key.startsWith(prefix)) {
        keysToRemove.push(key);
      }
    }
    
    keysToRemove.forEach(key => {
      this.storage.removeItem(key);
    });
  }
  
  // 获取所有数据
  getAll() {
    const result = {};
    const prefix = this.prefix;
    
    for (let i = 0; i < this.storage.length; i++) {
      const key = this.storage.key(i);
      if (key.startsWith(prefix)) {
        const realKey = key.slice(prefix.length);
        result[realKey] = this.get(realKey);
      }
    }
    
    return result;
  }
  
  // 监控存储使用情况
  getUsage() {
    let total = 0;
    const prefix = this.prefix;
    
    for (let i = 0; i < this.storage.length; i++) {
      const key = this.storage.key(i);
      if (key.startsWith(prefix)) {
        const value = this.storage.getItem(key);
        total += (key.length + value.length) * 2; // UTF-16 每个字符2字节
      }
    }
    
    return {
      bytes: total,
      kilobytes: (total / 1024).toFixed(2),
      megabytes: (total / (1024 * 1024)).toFixed(4)
    };
  }
}

// 使用示例
const storage = new StorageManager(localStorage, 'myapp_');

// 设置带过期时间的数据
storage.setWithExpiry('token', 'abc123', 30 * 60 * 1000); // 30分钟

// 获取数据(自动处理过期)
const token = storage.getWithExpiry('token');

// 批量操作
storage.setMany({
  theme: 'dark',
  language: 'zh-CN',
  settings: { notifications: true }
});

// 获取使用情况
console.log('存储使用:', storage.getUsage());

5.2 性能优化策略

javascript

// 1. 防抖存储(避免频繁写入)
class DebouncedStorage {
  constructor(delay = 1000) {
    this.delay = delay;
    this.timeouts = new Map();
  }
  
  debounceSet(key, value) {
    // 清除之前的定时器
    if (this.timeouts.has(key)) {
      clearTimeout(this.timeouts.get(key));
    }
    
    // 设置新的定时器
    const timeout = setTimeout(() => {
      localStorage.setItem(key, JSON.stringify(value));
      this.timeouts.delete(key);
    }, this.delay);
    
    this.timeouts.set(key, timeout);
  }
}

// 2. 增量更新大型对象
class IncrementalStorage {
  static updateLargeObject(key, updates) {
    try {
      const existing = JSON.parse(localStorage.getItem(key) || '{}');
      const updated = { ...existing, ...updates };
      localStorage.setItem(key, JSON.stringify(updated));
    } catch (error) {
      console.error('更新失败:', error);
    }
  }
}

// 3. 压缩存储
class CompressionStorage {
  static compress(data) {
    // 简单的压缩示例:移除空格和换行
    return JSON.stringify(data).replace(/\s+/g, '');
  }
  
  static setCompressed(key, data) {
    const compressed = this.compress(data);
    localStorage.setItem(key, compressed);
  }
  
  static getCompressed(key) {
    const compressed = localStorage.getItem(key);
    try {
      return JSON.parse(compressed);
    } catch {
      return compressed;
    }
  }
}

5.3 安全考虑

javascript

class SecureStorage {
  constructor(secretKey) {
    this.secretKey = secretKey;
  }
  
  // 简单加密(生产环境应使用更安全的加密算法)
  encrypt(text) {
    // 这里使用简单的 XOR 加密作为示例
    let result = '';
    for (let i = 0; i < text.length; i++) {
      const charCode = text.charCodeAt(i) ^ this.secretKey.charCodeAt(i % this.secretKey.length);
      result += String.fromCharCode(charCode);
    }
    return btoa(result); // Base64 编码
  }
  
  decrypt(encryptedText) {
    try {
      const text = atob(encryptedText);
      let result = '';
      for (let i = 0; i < text.length; i++) {
        const charCode = text.charCodeAt(i) ^ this.secretKey.charCodeAt(i % this.secretKey.length);
        result += String.fromCharCode(charCode);
      }
      return result;
    } catch {
      return null;
    }
  }
  
  setSecure(key, value) {
    const encrypted = this.encrypt(JSON.stringify(value));
    localStorage.setItem(key, encrypted);
  }
  
  getSecure(key) {
    const encrypted = localStorage.getItem(key);
    if (!encrypted) return null;
    
    const decrypted = this.decrypt(encrypted);
    if (!decrypted) return null;
    
    try {
      return JSON.parse(decrypted);
    } catch {
      return decrypted;
    }
  }
  
  // 安全清理
  static secureClear() {
    // 覆盖数据后再删除
    for (let i = 0; i < localStorage.length; i++) {
      const key = localStorage.key(i);
      localStorage.setItem(key, '0'.repeat(1024)); // 用垃圾数据覆盖
      localStorage.removeItem(key);
    }
  }
}

六、浏览器兼容性与限制

6.1 兼容性处理

javascript

// 检测 Storage 支持
function isStorageSupported() {
  try {
    const testKey = '__storage_test__';
    localStorage.setItem(testKey, testKey);
    localStorage.removeItem(testKey);
    return true;
  } catch (e) {
    return e instanceof DOMException && (
      // 所有现代浏览器
      e.code === 22 ||
      e.code === 1014 ||
      // Firefox
      e.name === 'QuotaExceededError' ||
      e.name === 'NS_ERROR_DOM_QUOTA_REACHED'
    ) && localStorage.length > 0;
  }
}

// 降级方案:使用 Cookie 或内存存储
class FallbackStorage {
  constructor() {
    this.supported = isStorageSupported();
    this.memoryStore = {};
  }
  
  setItem(key, value) {
    if (this.supported) {
      localStorage.setItem(key, value);
    } else if (typeof document !== 'undefined') {
      // 降级到 Cookie
      document.cookie = `${encodeURIComponent(key)}=${encodeURIComponent(value)}; path=/`;
    } else {
      // 降级到内存
      this.memoryStore[key] = value;
    }
  }
  
  getItem(key) {
    if (this.supported) {
      return localStorage.getItem(key);
    } else if (typeof document !== 'undefined') {
      // 从 Cookie 读取
      const cookies = document.cookie.split(';');
      for (let cookie of cookies) {
        const [cookieKey, cookieValue] = cookie.trim().split('=');
        if (decodeURIComponent(cookieKey) === key) {
          return decodeURIComponent(cookieValue);
        }
      }
      return null;
    } else {
      return this.memoryStore[key] || null;
    }
  }
}

6.2 浏览器特定限制

javascript

const storageLimits = {
  // 各浏览器大致限制(实际可能因设置而异)
  'Chrome': {
    localStorage: '10MB',
    sessionStorage: '10MB',
    note: '可配置,默认通常为10MB'
  },
  'Firefox': {
    localStorage: '10MB',
    sessionStorage: '10MB',
    note: '可扩展'
  },
  'Safari': {
    localStorage: '5MB',
    sessionStorage: '5MB',
    note: '移动版更小'
  },
  'Edge': {
    localStorage: '10MB',
    sessionStorage: '10MB',
    note: '与Chrome类似'
  }
};

// 检测实际可用空间
function estimateAvailableSpace() {
  const testKey = 'sizeTest';
  let total = '';
  
  try {
    // 尝试填充直到出错
    for (let i = 0; i < 10000; i++) {
      total += new Array(1024).join('x'); // 添加约1KB
      localStorage.setItem(testKey, total);
    }
  } catch (e) {
    console.log('可用空间估算:', Math.round(total.length / 1024), 'KB');
    localStorage.removeItem(testKey);
  }
}

七、实际应用案例

7.1 购物车实现

javascript

class ShoppingCart {
  constructor(storageKey = 'shopping_cart') {
    this.storageKey = storageKey;
    this.items = this.loadCart();
  }
  
  loadCart() {
    const cartData = localStorage.getItem(this.storageKey);
    return cartData ? JSON.parse(cartData) : [];
  }
  
  saveCart() {
    localStorage.setItem(this.storageKey, JSON.stringify(this.items));
    this.dispatchCartUpdate();
  }
  
  addItem(product, quantity = 1) {
    const existingIndex = this.items.findIndex(item => 
      item.id === product.id && item.variant === product.variant
    );
    
    if (existingIndex > -1) {
      this.items[existingIndex].quantity += quantity;
    } else {
      this.items.push({
        ...product,
        quantity,
        addedAt: new Date().toISOString()
      });
    }
    
    this.saveCart();
  }
  
  removeItem(productId, variant = null) {
    this.items = this.items.filter(item => 
      !(item.id === productId && (!variant || item.variant === variant))
    );
    this.saveCart();
  }
  
  updateQuantity(productId, quantity, variant = null) {
    const item = this.items.find(item => 
      item.id === productId && (!variant || item.variant === variant)
    );
    
    if (item) {
      if (quantity <= 0) {
        this.removeItem(productId, variant);
      } else {
        item.quantity = quantity;
        this.saveCart();
      }
    }
  }
  
  clear() {
    this.items = [];
    this.saveCart();
  }
  
  getTotal() {
    return this.items.reduce((total, item) => {
      return total + (item.price * item.quantity);
    }, 0);
  }
  
  getItemCount() {
    return this.items.reduce((count, item) => count + item.quantity, 0);
  }
  
  dispatchCartUpdate() {
    // 自定义事件,通知其他组件购物车已更新
    const event = new CustomEvent('cartUpdated', {
      detail: { cart: this.items }
    });
    window.dispatchEvent(event);
    
    // 同时通过 storage 事件通知其他标签页
    localStorage.setItem('cart_last_update', Date.now().toString());
  }
}

// 使用示例
const cart = new ShoppingCart();
cart.addItem({
  id: 'product-1',
  name: '商品A',
  price: 100,
  variant: '红色'
}, 2);

console.log('购物车总价:', cart.getTotal());

7.2 用户偏好设置

javascript

class UserPreferences {
  constructor() {
    this.preferences = this.loadPreferences();
    this.initEventListeners();
  }
  
  loadPreferences() {
    const defaults = {
      theme: 'light',
      language: navigator.language || 'zh-CN',
      fontSize: 16,
      notifications: true,
      autoSave: true,
      reduceMotion: false
    };
    
    const saved = localStorage.getItem('user_preferences');
    return saved ? { ...defaults, ...JSON.parse(saved) } : defaults;
  }
  
  savePreferences() {
    localStorage.setItem('user_preferences', JSON.stringify(this.preferences));
    this.dispatchPreferenceChange();
  }
  
  updatePreference(key, value) {
    this.preferences[key] = value;
    this.savePreferences();
  }
  
  getPreference(key) {
    return this.preferences[key];
  }
  
  resetToDefaults() {
    const defaults = this.loadPreferences();
    Object.keys(this.preferences).forEach(key => {
      if (defaults[key] !== undefined) {
        this.preferences[key] = defaults[key];
      }
    });
    this.savePreferences();
  }
  
  initEventListeners() {
    // 监听其他标签页的偏好设置变化
    window.addEventListener('storage', (event) => {
      if (event.key === 'user_preferences' && event.newValue) {
        try {
          const newPrefs = JSON.parse(event.newValue);
          this.preferences = newPrefs;
          this.applyPreferences();
        } catch (e) {
          console.error('加载偏好设置失败:', e);
        }
      }
    });
  }
  
  applyPreferences() {
    // 应用主题
    document.documentElement.setAttribute('data-theme', this.preferences.theme);
    
    // 应用字体大小
    document.documentElement.style.fontSize = `${this.preferences.fontSize}px`;
    
    // 应用其他设置
    if (this.preferences.reduceMotion) {
      document.documentElement.classList.add('reduce-motion');
    } else {
      document.documentElement.classList.remove('reduce-motion');
    }
    
    this.dispatchPreferenceChange();
  }
  
  dispatchPreferenceChange() {
    const event = new CustomEvent('preferencesChanged', {
      detail: { preferences: this.preferences }
    });
    window.dispatchEvent(event);
  }
}

// 使用示例
const prefs = new UserPreferences();

// 更新设置
prefs.updatePreference('theme', 'dark');
prefs.updatePreference('fontSize', 18);

// 监听设置变化
window.addEventListener('preferencesChanged', (event) => {
  console.log('设置已更新:', event.detail.preferences);
});

八、调试与监控

8.1 调试工具

javascript

// Storage 调试助手
class StorageDebugger {
  static logAllStorage() {
    console.group('LocalStorage 内容');
    for (let i = 0; i < localStorage.length; i++) {
      const key = localStorage.key(i);
      const value = localStorage.getItem(key);
      console.log(`${key}:`, value);
    }
    console.groupEnd();
    
    console.group('SessionStorage 内容');
    for (let i = 0; i < sessionStorage.length; i++) {
      const key = sessionStorage.key(i);
      const value = sessionStorage.getItem(key);
      console.log(`${key}:`, value);
    }
    console.groupEnd();
  }
  
  static monitorChanges() {
    // 监控 localStorage 变化
    const originalSetItem = localStorage.setItem;
    localStorage.setItem = function(key, value) {
      console.log(`localStorage.setItem: ${key} =`, value);
      return originalSetItem.apply(this, arguments);
    };
    
    const originalRemoveItem = localStorage.removeItem;
    localStorage.removeItem = function(key) {
      console.log(`localStorage.removeItem: ${key}`);
      return originalRemoveItem.apply(this, arguments);
    };
  }
  
  static exportStorage() {
    const exportData = {};
    
    for (let i = 0; i < localStorage.length; i++) {
      const key = localStorage.key(i);
      exportData[key] = localStorage.getItem(key);
    }
    
    const dataStr = JSON.stringify(exportData, null, 2);
    const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
    
    const exportFileDefaultName = 'localStorage_backup.json';
    const linkElement = document.createElement('a');
    linkElement.setAttribute('href', dataUri);
    linkElement.setAttribute('download', exportFileDefaultName);
    linkElement.click();
  }
  
  static importStorage(file) {
    const reader = new FileReader();
    reader.onload = function(e) {
      try {
        const data = JSON.parse(e.target.result);
        Object.entries(data).forEach(([key, value]) => {
          localStorage.setItem(key, value);
        });
        console.log('导入成功');
      } catch (error) {
        console.error('导入失败:', error);
      }
    };
    reader.readAsText(file);
  }
}

8.2 性能监控

javascript

class StoragePerformanceMonitor {
  constructor() {
    this.metrics = {
      writes: 0,
      reads: 0,
      deletes: 0,
      totalBytesWritten: 0,
      startTime: Date.now()
    };
    
    this.setupProxies();
  }
  
  setupProxies() {
    // 代理 localStorage
    const originalSetItem = localStorage.setItem;
    localStorage.setItem = (key, value) => {
      this.metrics.writes++;
      this.metrics.totalBytesWritten += (key.length + value.length) * 2;
      
      const start = performance.now();
      const result = originalSetItem.call(localStorage, key, value);
      const duration = performance.now() - start;
      
      console.log(`localStorage.setItem 耗时: ${duration.toFixed(2)}ms`);
      return result;
    };
    
    // 同样代理其他方法...
  }
  
  getReport() {
    const uptime = Date.now() - this.metrics.startTime;
    const hours = uptime / (1000 * 60 * 60);
    
    return {
      ...this.metrics,
      uptimeHours: hours.toFixed(2),
      avgWritesPerHour: (this.metrics.writes / hours).toFixed(2),
      avgReadsPerHour: (this.metrics.reads / hours).toFixed(2),
      avgBytesPerHour: (this.metrics.totalBytesWritten / hours).toFixed(2)
    };
  }
}

九、常见问题与解决方案

9.1 常见问题

javascript

// 1. 数据类型问题
function ensureStringStorage() {
  // 错误示例
  localStorage.setItem('number', 123); // 会被转为字符串 "123"
  localStorage.setItem('object', { a: 1 }); // 会存储为 "[object Object]"
  localStorage.setItem('null', null); // 会存储为 "null"
  localStorage.setItem('undefined', undefined); // 会存储为 "undefined"
  
  // 正确做法
  localStorage.setItem('number', JSON.stringify(123));
  localStorage.setItem('object', JSON.stringify({ a: 1 }));
  localStorage.setItem('null', JSON.stringify(null));
  localStorage.setItem('undefined', JSON.stringify(undefined));
}

// 2. 容量问题解决方案
class SmartStorage {
  constructor(maxSize = 5 * 1024 * 1024) { // 5MB
    this.maxSize = maxSize;
  }
  
  setItem(key, value) {
    const itemSize = (key.length + value.length) * 2;
    const currentSize = this.getCurrentSize();
    
    if (currentSize + itemSize > this.maxSize) {
      this.cleanupOldItems(itemSize);
    }
    
    localStorage.setItem(key, value);
  }
  
  getCurrentSize() {
    let total = 0;
    for (let i = 0; i < localStorage.length; i++) {
      const key = localStorage.key(i);
      const value = localStorage.getItem(key);
      total += (key.length + value.length) * 2;
    }
    return total;
  }
  
  cleanupOldItems(neededSpace) {
    // 基于 LRU(最近最少使用)策略清理
    const items = [];
    
    for (let i = 0; i < localStorage.length; i++) {
      const key = localStorage.key(i);
      const value = localStorage.getItem(key);
      // 假设每个键都有时间戳元数据
      try {
        const meta = JSON.parse(localStorage.getItem(`meta_${key}`)) || {};
        items.push({
          key,
          value,
          size: (key.length + value.length) * 2,
          lastAccessed: meta.lastAccessed || 0
        });
      } catch {
        items.push({
          key,
          value,
          size: (key.length + value.length) * 2,
          lastAccessed: 0
        });
      }
    }
    
    // 按最后访问时间排序
    items.sort((a, b) => a.lastAccessed - b.lastAccessed);
    
    let freedSpace = 0;
    for (const item of items) {
      if (freedSpace >= neededSpace) break;
      
      localStorage.removeItem(item.key);
      localStorage.removeItem(`meta_${item.key}`);
      freedSpace += item.size;
    }
  }
}

9.2 最佳实践总结

  1. 数据序列化

javascript

// 总是序列化非字符串数据
const data = { user: '张三', score: 100 };
localStorage.setItem('gameData', JSON.stringify(data));

// 总是反序列化读取
const stored = JSON.parse(localStorage.getItem('gameData') || '{}');
  1. 错误处理

javascript

function safeStorageOperation(operation) {
  try {
    return operation();
  } catch (error) {
    if (error.name === 'QuotaExceededError') {
      // 处理存储空间不足
      console.warn('存储空间不足');
      return false;
    }
    if (error.name === 'SecurityError') {
      // 处理安全限制(如隐私模式)
      console.warn('存储访问被拒绝');
      return false;
    }
    throw error;
  }
}

// 使用
safeStorageOperation(() => {
  localStorage.setItem('key', 'value');
});
  1. 数据验证

javascript

class ValidatedStorage {
  static setItem(key, value, schema) {
    // 使用 JSON Schema 验证数据
    if (schema && !this.validate(value, schema)) {
      throw new Error(`数据不符合schema: ${key}`);
    }
    
    localStorage.setItem(key, JSON.stringify({
      value,
      timestamp: Date.now(),
      version: '1.0'
    }));
  }
  
  static validate(data, schema) {
    // 简单的验证示例
    if (schema.type === 'object' && typeof data !== 'object') {
      return false;
    }
    if (schema.required) {
      for (const field of schema.required) {
        if (data[field] === undefined) {
          return false;
        }
      }
    }
    return true;
  }
}

十、未来发展与替代方案

10.1 IndexedDB(大容量存储替代)

javascript

// 对于超过 10MB 的数据,考虑使用 IndexedDB
const openDB = () => {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('MyDatabase', 1);
    
    request.onupgradeneeded = (event) => {
      const db = event.target.result;
      if (!db.objectStoreNames.contains('items')) {
        db.createObjectStore('items', { keyPath: 'id' });
      }
    };
    
    request.onsuccess = (event) => resolve(event.target.result);
    request.onerror = (event) => reject(event.target.error);
  });
};

// 使用示例
async function storeLargeData(data) {
  const db = await openDB();
  const transaction = db.transaction('items', 'readwrite');
  const store = transaction.objectStore('items');
  
  return new Promise((resolve, reject) => {
    const request = store.put(data);
    request.onsuccess = () => resolve();
    request.onerror = () => reject(request.error);
  });
}

10.2 Storage API 的局限与展望

虽然 Storage API 简单易用,但也有其局限性:

  1. 同步操作:可能阻塞主线程

  2. 容量限制:不适合存储大文件

  3. 仅限字符串:需要手动序列化

  4. 同源限制:无法跨域共享

未来的发展方向:

  • Storage Foundation API:更底层的存储访问

  • File System Access API:访问本地文件系统

  • Origin Private File System:每个源的私有文件系统

总结

Storage 接口是 Web 开发中客户端存储的重要工具。通过合理使用 localStorage 和 sessionStorage,结合良好的封装和错误处理,可以创建出色的用户体验。记住始终考虑数据安全、容量限制和浏览器兼容性,根据具体需求选择合适的存储策略。

更多推荐