首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >JavaScript原生实战手册 · 本地存储管理:带过期和类型安全的客户端存储

JavaScript原生实战手册 · 本地存储管理:带过期和类型安全的客户端存储

作者头像
前端达人
发布2025-10-09 12:38:44
发布2025-10-09 12:38:44
4200
代码可运行
举报
文章被收录于专栏:前端达人前端达人
运行总次数:0
代码可运行

localStorage只能存字符串?数据永不过期?一个强大的存储管理器让你的客户端存储更智能、更安全!

在现代Web应用开发中,客户端存储已经成为提升用户体验的重要手段:保存用户设置、缓存API数据、记录浏览历史、离线功能支持等。但原生的localStorage API存在许多局限性:只能存储字符串、没有过期机制、容易出现类型转换错误、缺少批量操作等。今天我们就来打造一个功能完备的本地存储管理器,解决这些痛点问题。

生活中的本地存储应用场景

场景一:购物网站的购物车

想象你在开发一个电商网站的购物车功能:

代码语言:javascript
代码运行次数:0
运行
复制
用户添加商品到购物车 → 保存到本地存储
用户关闭浏览器 → 数据依然保留
用户第二天打开网站 → 购物车内容完好无损
但如果是临时优惠商品 → 需要在24小时后自动清除

这种场景需要既能持久保存数据,又能自动过期清理的存储机制。

场景二:用户个性化设置

在管理后台中保存用户的个性化配置:

代码语言:javascript
代码运行次数:0
运行
复制
用户设置 = {
  theme: 'dark',           // 主题偏好
  language: 'zh-CN',       // 语言设置
  sidebarCollapsed: true,  // 侧边栏状态
  gridPageSize: 20,        // 表格每页显示条数
  recentSearches: ['关键词1', '关键词2'] // 最近搜索
}

这些设置需要:

  • 保持原有的数据类型(布尔值、数字、数组)
  • 长期保存(用户下次访问时依然有效)
  • 支持批量读写操作

场景三:API数据缓存

为了提升应用性能,经常需要缓存API返回的数据:

代码语言:javascript
代码运行次数:0
运行
复制
缓存策略 = {
  用户信息: 30分钟过期,
  商品列表: 10分钟过期,
  系统配置: 24小时过期,
  临时数据: 5分钟过期
}

不同类型的数据需要不同的过期时间,过期后应该自动清理以节省存储空间。

原生localStorage的痛点

痛点一:只能存储字符串

代码语言:javascript
代码运行次数:0
运行
复制
// 原生localStorage的限制
const userInfo = { name: '张三', age: 30, isVip: true };

// 存储时需要手动序列化
localStorage.setItem('user', JSON.stringify(userInfo));

// 读取时需要手动反序列化
const stored = localStorage.getItem('user');
const parsed = stored ? JSON.parse(stored) : null;

// 容易出错的类型判断
if (parsed && parsed.isVip === true) { // 需要严格比较
  console.log('VIP用户');
}

这种方式的问题:

  • 每次都要手动序列化/反序列化
  • 容易忘记类型转换,导致bug
  • 错误处理复杂

痛点二:没有过期机制

代码语言:javascript
代码运行次数:0
运行
复制
// 手动实现过期功能,代码重复且容易出错
function setWithExpiry(key, value, minutes) {
const item = {
    value: value,
    expiry: Date.now() + minutes * 60 * 1000
  };
  localStorage.setItem(key, JSON.stringify(item));
}

function getWithExpiry(key) {
const itemStr = localStorage.getItem(key);
if (!itemStr) returnnull;

const item = JSON.parse(itemStr);
if (Date.now() > item.expiry) {
    localStorage.removeItem(key);
    returnnull;
  }

return item.value;
}

// 每个功能都要重复写这样的代码...

痛点三:存储空间管理困难

代码语言:javascript
代码运行次数:0
运行
复制
// 难以管理存储空间
try {
  localStorage.setItem('large-data', JSON.stringify(hugeObject));
} catch (e) {
  if (e.name === 'QuotaExceededError') {
    // 存储空间不足,但不知道如何清理
    console.error('存储空间不足!');
    
    // 手动清理?但清理哪些数据?
    // localStorage.clear(); // 太粗暴,会删除所有数据
  }
}

痛点四:批量操作不便

代码语言:javascript
代码运行次数:0
运行
复制
// 批量保存用户设置,需要多次调用
const settings = {
theme: 'dark',
language: 'zh-CN',
notifications: true,
autoSave: false
};

// 只能一个一个保存
Object.entries(settings).forEach(([key, value]) => {
  localStorage.setItem(`setting_${key}`, JSON.stringify(value));
});

// 读取时也需要一个一个读取
const theme = JSON.parse(localStorage.getItem('setting_theme') || '""');
const language = JSON.parse(localStorage.getItem('setting_language') || '""');
// ... 代码重复且冗长

我们的本地存储管理器

现在让我们来实现一个功能完备的存储管理器:

代码语言:javascript
代码运行次数:0
运行
复制
class StorageManager {
constructor(prefix = 'app_', options = {}) {
    this.prefix = prefix;
    this.options = {
      maxSize: options.maxSize || 5 * 1024 * 1024, // 5MB默认限制
      autoCleanup: options.autoCleanup !== false,   // 默认开启自动清理
      compressionThreshold: options.compressionThreshold || 1024, // 1KB以上压缩
      ...options
    };
    
    this.isSupported = this.checkSupport();
    
    // 初始化时进行一次清理
    if (this.isSupported && this.options.autoCleanup) {
      this.cleanup();
    }
  }

// 检查localStorage支持情况
  checkSupport() {
    try {
      const test = '__storage_test__';
      localStorage.setItem(test, test);
      localStorage.removeItem(test);
      returntrue;
    } catch (e) {
      console.warn('localStorage不支持:', e.message);
      returnfalse;
    }
  }

// 存储数据(支持自动过期)
set(key, value, expirationMinutes = null) {
    if (!this.isSupported) {
      console.warn('localStorage不支持,无法保存数据');
      returnfalse;
    }
    
    // 构造存储项
    const item = {
      value,
      type: this.getValueType(value),
      timestamp: Date.now(),
      expiration: expirationMinutes ? Date.now() + (expirationMinutes * 60 * 1000) : null,
      compressed: false
    };
    
    let itemString = JSON.stringify(item);
    
    // 大数据压缩(如果支持)
    if (itemString.length > this.options.compressionThreshold && this.supportsCompression()) {
      try {
        itemString = this.compress(itemString);
        item.compressed = true;
      } catch (e) {
        console.warn('数据压缩失败:', e.message);
      }
    }
    
    try {
      localStorage.setItem(this.prefix + key, itemString);
      returntrue;
    } catch (e) {
      if (e.name === 'QuotaExceededError') {
        console.warn('存储空间不足,尝试清理过期数据...');
        
        // 清理过期数据后重试
        const cleaned = this.cleanup();
        if (cleaned > 0) {
          try {
            localStorage.setItem(this.prefix + key, itemString);
            console.log(`清理了${cleaned}个过期项,保存成功`);
            returntrue;
          } catch (e2) {
            console.error('清理后仍然无法保存:', e2.message);
          }
        }
        
        // 如果还是不行,清理最旧的数据
        returnthis.forceCleanupAndRetry(key, itemString);
      } else {
        console.error('保存数据失败:', e.message);
        returnfalse;
      }
    }
  }

// 获取数据(自动处理过期和类型转换)
get(key, defaultValue = null) {
    if (!this.isSupported) return defaultValue;
    
    try {
      let itemStr = localStorage.getItem(this.prefix + key);
      if (!itemStr) return defaultValue;
      
      // 如果数据被压缩,先解压
      if (itemStr.startsWith('compressed:')) {
        itemStr = this.decompress(itemStr);
      }
      
      const item = JSON.parse(itemStr);
      
      // 检查过期时间
      if (item.expiration && Date.now() > item.expiration) {
        this.remove(key);
        return defaultValue;
      }
      
      // 类型转换(处理Date对象等特殊类型)
      return this.restoreValue(item.value, item.type);
      
    } catch (e) {
      console.warn(`读取数据失败 (${key}):`, e.message);
      this.remove(key); // 删除损坏的数据
      return defaultValue;
    }
  }

// 删除数据
  remove(key) {
    if (!this.isSupported) returnfalse;
    localStorage.removeItem(this.prefix + key);
    return true;
  }

// 检查数据是否存在且未过期
  has(key) {
    return this.get(key, Symbol('not-found')) !== Symbol('not-found');
  }

// 清空所有应用数据
  clear() {
    if (!this.isSupported) returnfalse;
    
    const keys = Object.keys(localStorage)
      .filter(key => key.startsWith(this.prefix));
    
    keys.forEach(key => localStorage.removeItem(key));
    
    console.log(`清空了${keys.length}个存储项`);
    returntrue;
  }

// 清理过期数据
  cleanup() {
    if (!this.isSupported) return0;
    
    let cleaned = 0;
    const keys = Object.keys(localStorage)
      .filter(key => key.startsWith(this.prefix));
    
    keys.forEach(key => {
      try {
        let itemStr = localStorage.getItem(key);
        
        // 处理压缩数据
        if (itemStr.startsWith('compressed:')) {
          itemStr = this.decompress(itemStr);
        }
        
        const item = JSON.parse(itemStr);
        
        // 删除过期数据
        if (item.expiration && Date.now() > item.expiration) {
          localStorage.removeItem(key);
          cleaned++;
        }
      } catch (e) {
        // 删除损坏的数据
        localStorage.removeItem(key);
        cleaned++;
      }
    });
    
    if (cleaned > 0) {
      console.log(`清理了${cleaned}个过期或损坏的存储项`);
    }
    
    return cleaned;
  }

// 获取存储使用情况
  usage() {
    if (!this.isSupported) return { used: 0, total: 0, items: 0 };
    
    let used = 0;
    let items = 0;
    
    Object.keys(localStorage)
      .filter(key => key.startsWith(this.prefix))
      .forEach(key => {
        const value = localStorage.getItem(key);
        used += key.length + (value ? value.length : 0);
        items++;
      });
    
    return {
      used,
      items,
      usedFormatted: this.formatBytes(used),
      estimatedTotal: this.formatBytes(5 * 1024 * 1024), // localStorage通常5MB限制
      usagePercentage: ((used / (5 * 1024 * 1024)) * 100).toFixed(2) + '%'
    };
  }

// 批量操作
  setMultiple(items, expirationMinutes = null) {
    const results = {};
    let successCount = 0;
    
    Object.entries(items).forEach(([key, value]) => {
      const success = this.set(key, value, expirationMinutes);
      results[key] = success;
      if (success) successCount++;
    });
    
    console.log(`批量保存: ${successCount}/${Object.keys(items).length}个成功`);
    return results;
  }

  getMultiple(keys, defaultValue = null) {
    const results = {};
    keys.forEach(key => {
      results[key] = this.get(key, defaultValue);
    });
    return results;
  }

// 获取所有存储的键
  keys() {
    if (!this.isSupported) return [];
    
    returnObject.keys(localStorage)
      .filter(key => key.startsWith(this.prefix))
      .map(key => key.substring(this.prefix.length));
  }

// 获取存储项数量
  size() {
    return this.keys().length;
  }

// 工具方法:获取值的类型
  getValueType(value) {
    if (value === null) return'null';
    if (value === undefined) return'undefined';
    if (value instanceofDate) return'date';
    if (Array.isArray(value)) return'array';
    return typeof value;
  }

// 工具方法:恢复值的类型
  restoreValue(value, type) {
    switch (type) {
      case'date':
        returnnewDate(value);
      case'undefined':
        returnundefined;
      case'null':
        returnnull;
      default:
        return value;
    }
  }

// 工具方法:格式化字节数
  formatBytes(bytes) {
    if (bytes === 0) return'0 B';
    const k = 1024;
    const sizes = ['B', 'KB', 'MB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  }

// 检查是否支持压缩
  supportsCompression() {
    return typeof CompressionStream !== 'undefined' || typeof pako !== 'undefined';
  }

// 压缩数据(简单实现)
  compress(data) {
    // 这里可以集成真正的压缩库,如pako
    // 为了演示,使用简单的base64编码标记
    return'compressed:' + btoa(encodeURIComponent(data));
  }

// 解压数据
  decompress(compressedData) {
    if (!compressedData.startsWith('compressed:')) {
      return compressedData;
    }
    
    try {
      return decodeURIComponent(atob(compressedData.substring(11)));
    } catch (e) {
      console.warn('解压数据失败:', e.message);
      return compressedData;
    }
  }

// 强制清理并重试保存
  forceCleanupAndRetry(key, itemString) {
    console.warn('开始强制清理最旧的数据...');
    
    // 获取所有存储项及其时间戳
    const items = [];
    Object.keys(localStorage)
      .filter(storageKey => storageKey.startsWith(this.prefix))
      .forEach(storageKey => {
        try {
          let itemStr = localStorage.getItem(storageKey);
          if (itemStr.startsWith('compressed:')) {
            itemStr = this.decompress(itemStr);
          }
          
          const item = JSON.parse(itemStr);
          items.push({
            key: storageKey,
            timestamp: item.timestamp || 0,
            size: storageKey.length + itemStr.length
          });
        } catch (e) {
          // 损坏的数据,标记为最旧
          items.push({
            key: storageKey,
            timestamp: 0,
            size: storageKey.length
          });
        }
      });
    
    // 按时间戳排序,删除最旧的数据
    items.sort((a, b) => a.timestamp - b.timestamp);
    
    let freedSpace = 0;
    let deletedCount = 0;
    
    for (const item of items) {
      localStorage.removeItem(item.key);
      freedSpace += item.size;
      deletedCount++;
      
      // 尝试保存新数据
      try {
        localStorage.setItem(this.prefix + key, itemString);
        console.log(`强制清理了${deletedCount}个旧数据项,释放${this.formatBytes(freedSpace)}空间`);
        returntrue;
      } catch (e) {
        // 继续清理更多数据
        if (deletedCount >= items.length) {
          break; // 已经清理完所有数据
        }
      }
    }
    
    console.error('即使清理所有旧数据也无法保存新数据');
    returnfalse;
  }

// 导出数据(用于备份)
export() {
    if (!this.isSupported) returnnull;
    
    const exportData = {
      timestamp: Date.now(),
      version: '1.0',
      prefix: this.prefix,
      data: {}
    };
    
    this.keys().forEach(key => {
      const value = this.get(key);
      if (value !== null) {
        exportData.data[key] = value;
      }
    });
    
    return exportData;
  }

// 导入数据(用于恢复)
import(exportData, options = {}) {
    if (!this.isSupported || !exportData || !exportData.data) {
      return { success: false, message: '导入数据无效' };
    }
    
    const { overwrite = false, expirationMinutes = null } = options;
    let imported = 0;
    let skipped = 0;
    
    Object.entries(exportData.data).forEach(([key, value]) => {
      // 如果不覆盖且key已存在,则跳过
      if (!overwrite && this.has(key)) {
        skipped++;
        return;
      }
      
      if (this.set(key, value, expirationMinutes)) {
        imported++;
      }
    });
    
    return {
      success: true,
      imported,
      skipped,
      message: `成功导入${imported}项数据${skipped > 0 ? `,跳过${skipped}项` : ''}`
    };
  }
}

基础功能展示

让我们看看这个存储管理器的基本使用:

代码语言:javascript
代码运行次数:0
运行
复制
// 创建存储管理器实例
const storage = new StorageManager('myApp_', {
autoCleanup: true,        // 自动清理过期数据
maxSize: 10 * 1024 * 1024// 10MB限制
});

// 存储不同类型的数据
storage.set('userInfo', {
name: '张三',
age: 30,
isVip: true,
lastLogin: newDate()
}, 60); // 1小时后过期

storage.set('settings', {
theme: 'dark',
language: 'zh-CN',
notifications: true,
sidebarCollapsed: false
}); // 永不过期

storage.set('tempData', '临时数据', 5); // 5分钟后过期

// 读取数据(自动类型转换)
const userInfo = storage.get('userInfo');
console.log('用户信息:', userInfo);
console.log('最后登录时间:', userInfo.lastLogin instanceofDate); // true

const theme = storage.get('settings').theme;
console.log('当前主题:', theme); // 'dark'

// 检查数据是否存在
if (storage.has('userInfo')) {
console.log('用户已登录');
}

// 批量操作
const multipleData = storage.getMultiple(['userInfo', 'settings', 'nonexistent']);
console.log('批量读取结果:', multipleData);

storage.setMultiple({
'cache1': { data: '缓存数据1' },
'cache2': { data: '缓存数据2' },
'cache3': { data: '缓存数据3' }
}, 30); // 所有缓存30分钟后过期

// 存储使用情况
const usage = storage.usage();
console.log('存储使用情况:', usage);
// 输出: { used: 1024, items: 5, usedFormatted: '1.00 KB', usagePercentage: '0.02%' }

// 清理过期数据
setTimeout(() => {
const cleaned = storage.cleanup();
console.log(`清理了${cleaned}个过期项`);
}, 6 * 60 * 1000); // 6分钟后清理

实际项目应用示例

1. 用户设置管理系统

代码语言:javascript
代码运行次数:0
运行
复制
class UserSettingsManager {
constructor(userId) {
    this.userId = userId;
    this.storage = new StorageManager(`user_${userId}_`, {
      autoCleanup: true
    });
    
    // 默认设置
    this.defaultSettings = {
      theme: 'light',
      language: 'zh-CN',
      timezone: 'Asia/Shanghai',
      dateFormat: 'YYYY-MM-DD',
      notifications: {
        email: true,
        push: true,
        sms: false
      },
      privacy: {
        showProfile: true,
        showEmail: false,
        allowFriendRequests: true
      },
      ui: {
        sidebarCollapsed: false,
        tablePageSize: 20,
        gridView: 'card'
      }
    };
    
    this.initSettings();
  }

// 初始化设置(合并默认设置和已保存设置)
  initSettings() {
    const savedSettings = this.storage.get('settings', {});
    this.settings = this.mergeSettings(this.defaultSettings, savedSettings);
    
    // 保存合并后的设置
    this.saveSettings();
  }

// 深度合并设置
  mergeSettings(defaults, saved) {
    const result = { ...defaults };
    
    Object.keys(saved).forEach(key => {
      if (typeof saved[key] === 'object' && saved[key] !== null && !Array.isArray(saved[key])) {
        result[key] = this.mergeSettings(defaults[key] || {}, saved[key]);
      } else {
        result[key] = saved[key];
      }
    });
    
    return result;
  }

// 获取设置值
get(path, defaultValue = null) {
    const keys = path.split('.');
    let value = this.settings;
    
    for (const key of keys) {
      if (value && typeof value === 'object' && key in value) {
        value = value[key];
      } else {
        return defaultValue;
      }
    }
    
    return value;
  }

// 设置值
set(path, value) {
    const keys = path.split('.');
    let current = this.settings;
    
    // 导航到最后一层
    for (let i = 0; i < keys.length - 1; i++) {
      const key = keys[i];
      if (!current[key] || typeof current[key] !== 'object') {
        current[key] = {};
      }
      current = current[key];
    }
    
    // 设置值
    current[keys[keys.length - 1]] = value;
    
    // 保存到存储
    this.saveSettings();
    
    // 触发设置变更事件
    this.onSettingChanged(path, value);
  }

// 批量设置
  setMultiple(settings) {
    Object.entries(settings).forEach(([path, value]) => {
      // 不触发单个事件,最后统一触发
      const keys = path.split('.');
      let current = this.settings;
      
      for (let i = 0; i < keys.length - 1; i++) {
        const key = keys[i];
        if (!current[key] || typeof current[key] !== 'object') {
          current[key] = {};
        }
        current = current[key];
      }
      
      current[keys[keys.length - 1]] = value;
    });
    
    this.saveSettings();
    this.onSettingsChanged(settings);
  }

// 重置为默认设置
  reset(section = null) {
    if (section) {
      this.settings[section] = { ...this.defaultSettings[section] };
    } else {
      this.settings = { ...this.defaultSettings };
    }
    
    this.saveSettings();
    this.onSettingsReset(section);
  }

// 保存设置
  saveSettings() {
    this.storage.set('settings', this.settings);
    this.storage.set('lastUpdated', newDate());
  }

// 导出设置
export() {
    return {
      userId: this.userId,
      settings: this.settings,
      exportDate: newDate().toISOString(),
      version: '1.0'
    };
  }

// 导入设置
import(settingsData) {
    if (!settingsData || !settingsData.settings) {
      thrownewError('无效的设置数据');
    }
    
    // 合并导入的设置
    this.settings = this.mergeSettings(this.defaultSettings, settingsData.settings);
    this.saveSettings();
    
    console.log('设置导入成功');
    this.onSettingsImported(settingsData);
  }

// 获取设置历史
  getHistory(limit = 10) {
    returnthis.storage.get('settingsHistory', []).slice(-limit);
  }

// 保存设置历史
  saveToHistory(action, changes) {
    const history = this.storage.get('settingsHistory', []);
    
    history.push({
      timestamp: newDate(),
      action,
      changes,
      snapshot: { ...this.settings }
    });
    
    // 只保留最近50条历史记录
    if (history.length > 50) {
      history.splice(0, history.length - 50);
    }
    
    this.storage.set('settingsHistory', history);
  }

// 事件回调方法(子类可重写)
  onSettingChanged(path, value) {
    console.log(`设置已更改: ${path} = `, value);
    this.saveToHistory('change', { [path]: value });
  }

  onSettingsChanged(settings) {
    console.log('批量设置已更改:', settings);
    this.saveToHistory('batch_change', settings);
  }

  onSettingsReset(section) {
    console.log('设置已重置:', section || '全部');
    this.saveToHistory('reset', { section: section || 'all' });
  }

  onSettingsImported(data) {
    console.log('设置已导入:', data);
    this.saveToHistory('import', { source: data.version });
  }

// 获取设置统计信息
  getStats() {
    const allKeys = this.getAllKeys(this.settings);
    const changedKeys = this.getChangedKeys(this.settings, this.defaultSettings);
    const lastUpdated = this.storage.get('lastUpdated');
    
    return {
      totalSettings: allKeys.length,
      customizedSettings: changedKeys.length,
      customizationRate: `${((changedKeys.length / allKeys.length) * 100).toFixed(1)}%`,
      lastUpdated: lastUpdated,
      storageUsage: this.storage.usage()
    };
  }
  
  // 获取所有键路径
  getAllKeys(obj, prefix = '') {
    let keys = [];
    
    Object.keys(obj).forEach(key => {
      const fullKey = prefix ? `${prefix}.${key}` : key;
      
      if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
        keys = keys.concat(this.getAllKeys(obj[key], fullKey));
      } else {
        keys.push(fullKey);
      }
    });
    
    return keys;
  }

  // 获取已更改的键
  getChangedKeys(current, defaults, prefix = '') {
    let changed = [];
    
    Object.keys(current).forEach(key => {
      const fullKey = prefix ? `${prefix}.${key}` : key;
      
      if (typeof current[key] === 'object' && current[key] !== null && !Array.isArray(current[key])) {
        if (defaults[key]) {
          changed = changed.concat(this.getChangedKeys(current[key], defaults[key], fullKey));
        } else {
          changed.push(fullKey);
        }
      } else {
        if (current[key] !== defaults[key]) {
          changed.push(fullKey);
        }
      }
    });
    
    return changed;
  }
}

// 使用示例
const userSettings = new UserSettingsManager('user123');

// 获取设置
console.log('当前主题:', userSettings.get('theme'));
console.log('通知设置:', userSettings.get('notifications'));
console.log('表格页面大小:', userSettings.get('ui.tablePageSize'));

// 修改设置
userSettings.set('theme', 'dark');
userSettings.set('notifications.email', false);
userSettings.set('ui.sidebarCollapsed', true);

// 批量修改设置
userSettings.setMultiple({
  'language': 'en-US',
  'timezone': 'America/New_York',
  'privacy.showProfile': false
});

// 导出设置
const exportedSettings = userSettings.export();
console.log('导出设置:', exportedSettings);

// 查看统计信息
const stats = userSettings.getStats();
console.log('设置统计:', stats);

// 查看历史记录
const history = userSettings.getHistory(5);
console.log('设置历史:', history);

2. API数据缓存系统

代码语言:javascript
代码运行次数:0
运行
复制
class APICache {
constructor(options = {}) {
    this.storage = new StorageManager('api_cache_', {
      autoCleanup: true,
      maxSize: options.maxSize || 50 * 1024 * 1024// 50MB缓存限制
    });
    
    this.defaultExpiry = options.defaultExpiry || 15; // 默认15分钟
    this.cacheStrategies = options.strategies || {};
    
    // 统计信息
    this.stats = {
      hits: 0,
      misses: 0,
      errors: 0
    };
    
    this.loadStats();
  }

// 生成缓存键
  generateCacheKey(url, params = {}, headers = {}) {
    // 创建一个唯一的键,考虑URL、参数和重要的请求头
    const keyData = {
      url: url.toLowerCase(),
      params: this.sortObject(params),
      headers: this.pickHeaders(headers)
    };
    
    const keyString = JSON.stringify(keyData);
    returnthis.hashString(keyString);
  }

// 获取缓存策略
  getCacheStrategy(url) {
    // 检查URL是否匹配任何策略
    for (const [pattern, strategy] ofObject.entries(this.cacheStrategies)) {
      if (this.matchPattern(url, pattern)) {
        return strategy;
      }
    }
    
    return { expiry: this.defaultExpiry, enabled: true };
  }

// 缓存GET请求
async cacheGet(url, options = {}) {
    const { params = {}, headers = {}, force = false } = options;
    const strategy = this.getCacheStrategy(url);
    
    if (!strategy.enabled) {
      returnthis.fetchWithoutCache(url, { params, headers });
    }
    
    const cacheKey = this.generateCacheKey(url, params, headers);
    
    // 如果不是强制刷新,先检查缓存
    if (!force) {
      const cached = this.storage.get(cacheKey);
      if (cached) {
        this.stats.hits++;
        this.saveStats();
        
        console.log(`缓存命中: ${url}`);
        
        // 返回缓存数据,同时异步检查是否需要后台更新
        if (strategy.backgroundRefresh && this.shouldBackgroundRefresh(cached)) {
          this.backgroundRefresh(url, options, cacheKey);
        }
        
        return cached.data;
      }
    }
    
    // 缓存未命中,发起请求
    this.stats.misses++;
    this.saveStats();
    
    console.log(`缓存未命中: ${url}`);
    
    try {
      const data = awaitthis.fetchWithoutCache(url, { params, headers });
      
      // 缓存结果
      const cacheItem = {
        data,
        url,
        timestamp: Date.now(),
        headers: headers
      };
      
      this.storage.set(cacheKey, cacheItem, strategy.expiry);
      
      return data;
      
    } catch (error) {
      this.stats.errors++;
      this.saveStats();
      
      // 如果请求失败,尝试返回过期的缓存数据(如果允许)
      if (strategy.fallbackToStale) {
        const staleCache = this.storage.get(cacheKey + '_stale');
        if (staleCache) {
          console.warn(`请求失败,返回过期缓存: ${url}`);
          return staleCache.data;
        }
      }
      
      throw error;
    }
  }

// 实际的HTTP请求(模拟)
async fetchWithoutCache(url, { params = {}, headers = {} }) {
    // 构建完整URL
    const fullUrl = this.buildUrl(url, params);
    
    console.log(`发起HTTP请求: ${fullUrl}`);
    
    // 模拟网络请求
    const response = await fetch(fullUrl, { headers });
    
    if (!response.ok) {
      thrownewError(`HTTP ${response.status}: ${response.statusText}`);
    }
    
    return response.json();
  }

// 后台刷新缓存
async backgroundRefresh(url, options, cacheKey) {
    try {
      console.log(`后台刷新缓存: ${url}`);
      
      const data = awaitthis.fetchWithoutCache(url, options);
      const strategy = this.getCacheStrategy(url);
      
      const cacheItem = {
        data,
        url,
        timestamp: Date.now(),
        headers: options.headers || {}
      };
      
      this.storage.set(cacheKey, cacheItem, strategy.expiry);
      
    } catch (error) {
      console.warn(`后台刷新失败: ${url}`, error.message);
    }
  }

// 判断是否需要后台刷新
  shouldBackgroundRefresh(cached) {
    const age = Date.now() - cached.timestamp;
    const maxAge = 5 * 60 * 1000; // 5分钟
    return age > maxAge;
  }

// 预加载缓存
async preload(urls) {
    console.log(`预加载${urls.length}个URL的缓存`);
    
    const promises = urls.map(async (urlConfig) => {
      try {
        const { url, params, headers } = typeof urlConfig === 'string'
          ? { url: urlConfig, params: {}, headers: {} } 
          : urlConfig;
          
        awaitthis.cacheGet(url, { params, headers });
        return { url, success: true };
      } catch (error) {
        return { url: urlConfig.url || urlConfig, success: false, error: error.message };
      }
    });
    
    const results = awaitPromise.all(promises);
    const successful = results.filter(r => r.success).length;
    
    console.log(`预加载完成: ${successful}/${urls.length}个成功`);
    return results;
  }

// 清空指定URL模式的缓存
  clearPattern(pattern) {
    const keys = this.storage.keys();
    let cleared = 0;
    
    keys.forEach(key => {
      const cached = this.storage.get(key);
      if (cached && cached.url && this.matchPattern(cached.url, pattern)) {
        this.storage.remove(key);
        cleared++;
      }
    });
    
    console.log(`清空了${cleared}个匹配"${pattern}"的缓存项`);
    return cleared;
  }

// 获取缓存统计信息
  getStats() {
    const hitRate = this.stats.hits + this.stats.misses > 0
      ? ((this.stats.hits / (this.stats.hits + this.stats.misses)) * 100).toFixed(1) + '%'
      : '0%';
    
    return {
      ...this.stats,
      hitRate,
      cacheSize: this.storage.size(),
      storageUsage: this.storage.usage()
    };
  }

// 获取缓存详情
  getCacheDetails() {
    const keys = this.storage.keys();
    const details = [];
    
    keys.forEach(key => {
      const cached = this.storage.get(key);
      if (cached) {
        const age = Date.now() - cached.timestamp;
        details.push({
          key,
          url: cached.url,
          age: this.formatDuration(age),
          size: JSON.stringify(cached).length
        });
      }
    });
    
    return details.sort((a, b) => b.age - a.age);
  }

// 工具方法
  sortObject(obj) {
    const sorted = {};
    Object.keys(obj).sort().forEach(key => {
      sorted[key] = obj[key];
    });
    return sorted;
  }

  pickHeaders(headers) {
    // 只保留影响缓存的重要请求头
    const importantHeaders = ['authorization', 'accept', 'content-type'];
    const picked = {};
    
    importantHeaders.forEach(header => {
      if (headers[header]) {
        picked[header] = headers[header];
      }
    });
    
    return picked;
  }

  buildUrl(url, params) {
    if (Object.keys(params).length === 0) return url;
    
    const searchParams = new URLSearchParams();
    Object.entries(params).forEach(([key, value]) => {
      searchParams.append(key, value);
    });
    
    return`${url}${url.includes('?') ? '&' : '?'}${searchParams.toString()}`;
  }

  matchPattern(url, pattern) {
    // 支持简单的通配符模式匹配
    const regex = newRegExp(pattern.replace(/\*/g, '.*'));
    return regex.test(url);
  }

  hashString(str) {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash;
    }
    return`cache_${Math.abs(hash).toString(36)}`;
  }

  formatDuration(ms) {
    const seconds = Math.floor(ms / 1000);
    const minutes = Math.floor(seconds / 60);
    const hours = Math.floor(minutes / 60);
    
    if (hours > 0) return`${hours}小时${minutes % 60}分钟前`;
    if (minutes > 0) return`${minutes}分钟${seconds % 60}秒前`;
    return`${seconds}秒前`;
  }

  saveStats() {
    this.storage.set('_stats', this.stats);
  }

  loadStats() {
    const saved = this.storage.get('_stats');
    if (saved) {
      this.stats = { ...this.stats, ...saved };
    }
  }
}

// 使用示例
const apiCache = new APICache({
defaultExpiry: 10, // 默认10分钟过期
strategies: {
    '/api/users/*': { expiry: 30, backgroundRefresh: true }, // 用户数据30分钟过期
    '/api/config/*': { expiry: 60, fallbackToStale: true },  // 配置数据1小时过期
    '/api/realtime/*': { enabled: false }                    // 实时数据不缓存
  }
});

// 缓存API请求
asyncfunction loadUserProfile(userId) {
try {
    const userData = await apiCache.cacheGet(`/api/users/${userId}`, {
      headers: { 'Authorization': 'Bearer token123' }
    });
    
    console.log('用户数据:', userData);
    return userData;
  } catch (error) {
    console.error('加载用户资料失败:', error.message);
  }
}

// 带参数的API请求
asyncfunction loadProductList(category, page = 1) {
try {
    const products = await apiCache.cacheGet('/api/products', {
      params: { category, page, limit: 20 }
    });
    
    console.log('产品列表:', products);
    return products;
  } catch (error) {
    console.error('加载产品列表失败:', error.message);
  }
}

// 预加载缓存
apiCache.preload([
  { url: '/api/config/app', params: {} },
  { url: '/api/users/me', headers: { 'Authorization': 'Bearer token123' } },
'/api/categories'
]);

// 查看缓存统计
console.log('缓存统计:', apiCache.getStats());

// 定期清理过期缓存
setInterval(() => {
  apiCache.storage.cleanup();
}, 30 * 60 * 1000); // 每30分钟清理一次

性能优化和最佳实践

1. 存储空间监控

代码语言:javascript
代码运行次数:0
运行
复制
class StorageMonitor {
constructor(storage) {
    this.storage = storage;
    this.alerts = [];
    this.thresholds = {
      warning: 80,  // 80%时警告
      critical: 95// 95%时严重警告
    };
  }

// 监控存储使用情况
  monitor() {
    const usage = this.storage.usage();
    const usagePercent = parseFloat(usage.usagePercentage);
    
    if (usagePercent >= this.thresholds.critical) {
      this.handleCriticalUsage(usage);
    } elseif (usagePercent >= this.thresholds.warning) {
      this.handleWarningUsage(usage);
    }
    
    return usage;
  }

  handleWarningUsage(usage) {
    const alert = {
      level: 'warning',
      message: `存储空间使用率已达到${usage.usagePercentage}`,
      timestamp: newDate(),
      usage
    };
    
    this.alerts.push(alert);
    console.warn('存储空间警告:', alert.message);
  }

  handleCriticalUsage(usage) {
    const alert = {
      level: 'critical',
      message: `存储空间严重不足,使用率${usage.usagePercentage}`,
      timestamp: newDate(),
      usage
    };
    
    this.alerts.push(alert);
    console.error('存储空间严重警告:', alert.message);
    
    // 自动清理
    const cleaned = this.storage.cleanup();
    console.log(`自动清理了${cleaned}个过期项`);
  }

  getAlerts() {
    returnthis.alerts;
  }
}

2. 数据压缩优化

代码语言:javascript
代码运行次数:0
运行
复制
class CompressedStorageManager extends StorageManager {
constructor(prefix, options = {}) {
    super(prefix, options);
    this.compressionEnabled = options.compression !== false;
    this.compressionThreshold = options.compressionThreshold || 1024; // 1KB
  }

// 智能压缩
  compress(data) {
    const jsonStr = JSON.stringify(data);
    
    if (jsonStr.length < this.compressionThreshold) {
      return { compressed: false, data: jsonStr };
    }
    
    // 使用LZ字符串压缩算法(简化版)
    const compressed = this.lzCompress(jsonStr);
    
    // 如果压缩后更大,就不压缩
    if (compressed.length >= jsonStr.length) {
      return { compressed: false, data: jsonStr };
    }
    
    return { compressed: true, data: compressed };
  }

  decompress(compressedData) {
    if (!compressedData.compressed) {
      return compressedData.data;
    }
    
    returnthis.lzDecompress(compressedData.data);
  }

// 简化的LZ压缩(实际项目建议使用成熟的压缩库)
  lzCompress(str) {
    const dict = {};
    let data = (str + "").split("");
    let out = [];
    let currChar;
    let phrase = data[0];
    let code = 256;
    
    for (let i = 1; i < data.length; i++) {
      currChar = data[i];
      if (dict[phrase + currChar] != null) {
        phrase += currChar;
      } else {
        out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0));
        dict[phrase + currChar] = code;
        code++;
        phrase = currChar;
      }
    }
    
    out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0));
    return out;
  }

  lzDecompress(data) {
    const dict = {};
    let currChar = String.fromCharCode(data[0]);
    let oldPhrase = currChar;
    let out = [currChar];
    let code = 256;
    
    for (let i = 1; i < data.length; i++) {
      const currCode = data[i];
      let phrase;
      
      if (dict[currCode]) {
        phrase = dict[currCode];
      } elseif (currCode === code) {
        phrase = oldPhrase + currChar;
      } else {
        phrase = String.fromCharCode(currCode);
      }
      
      out.push(phrase);
      currChar = phrase.charAt(0);
      dict[code] = oldPhrase + currChar;
      code++;
      oldPhrase = phrase;
    }
    
    return out.join("");
  }
}

3. 异步操作优化

代码语言:javascript
代码运行次数:0
运行
复制
class AsyncStorageManager extends StorageManager {
constructor(prefix, options = {}) {
    super(prefix, options);
    this.operationQueue = [];
    this.processing = false;
  }

// 异步批量操作
async batchOperation(operations) {
    returnnewPromise((resolve) => {
      this.operationQueue.push({ operations, resolve });
      this.processQueue();
    });
  }

async processQueue() {
    if (this.processing || this.operationQueue.length === 0) {
      return;
    }
    
    this.processing = true;
    
    while (this.operationQueue.length > 0) {
      const batch = this.operationQueue.shift();
      const results = [];
      
      // 使用requestIdleCallback优化性能
      awaitthis.executeInIdle(() => {
        batch.operations.forEach(op => {
          switch (op.type) {
            case'set':
              results.push(this.set(op.key, op.value, op.expiry));
              break;
            case'get':
              results.push(this.get(op.key, op.defaultValue));
              break;
            case'remove':
              results.push(this.remove(op.key));
              break;
          }
        });
      });
      
      batch.resolve(results);
    }
    
    this.processing = false;
  }

  executeInIdle(callback) {
    returnnewPromise((resolve) => {
      if (typeof requestIdleCallback === 'function') {
        requestIdleCallback(() => {
          callback();
          resolve();
        });
      } else {
        setTimeout(() => {
          callback();
          resolve();
        }, 0);
      }
    });
  }
}

总结

通过我们自制的本地存储管理器,我们实现了:

核心优势:

  • 类型安全:自动处理序列化/反序列化,保持数据类型
  • 自动过期:智能的过期机制,防止数据过时
  • 空间管理:自动清理过期数据,智能处理存储空间不足
  • 批量操作:提高多数据操作的性能

强大功能:

  • ✅ 支持所有JavaScript数据类型(对象、数组、日期、布尔值等)
  • ✅ 灵活的过期时间设置(分钟级精度)
  • ✅ 智能的空间管理和清理机制
  • ✅ 完整的错误处理和降级方案
  • ✅ 详细的使用统计和监控

实际应用场景:

  • ✅ 用户设置管理:个性化配置的持久存储
  • ✅ API数据缓存:提升应用性能的智能缓存
  • ✅ 购物车功能:电商网站的数据持久化
  • ✅ 表单草稿:防止用户数据丢失

性能和可靠性保障:

  • ✅ 异步操作优化:避免阻塞主线程
  • ✅ 数据压缩:节省存储空间
  • ✅ 错误恢复:自动处理数据损坏
  • ✅ 监控告警:实时监控存储使用情况

这个存储管理器不仅解决了原生localStorage的所有痛点,还提供了企业级应用所需的可靠性和性能保障。无论是简单的设置保存还是复杂的缓存管理,都能轻松胜任。

掌握了这个工具,你就能在项目中自信地处理任何客户端存储需求,让用户数据更安全、应用性能更优秀!


《JavaScript原生实战手册》专栏持续更新中,下期预告:《异步重试机制:网络请求的可靠性保障》

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-08-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端达人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 生活中的本地存储应用场景
    • 场景一:购物网站的购物车
    • 场景二:用户个性化设置
    • 场景三:API数据缓存
  • 原生localStorage的痛点
    • 痛点一:只能存储字符串
    • 痛点二:没有过期机制
    • 痛点三:存储空间管理困难
    • 痛点四:批量操作不便
  • 我们的本地存储管理器
  • 基础功能展示
  • 实际项目应用示例
    • 1. 用户设置管理系统
    • 2. API数据缓存系统
  • 性能优化和最佳实践
    • 1. 存储空间监控
    • 2. 数据压缩优化
    • 3. 异步操作优化
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档