localStorage只能存字符串?数据永不过期?一个强大的存储管理器让你的客户端存储更智能、更安全!
在现代Web应用开发中,客户端存储已经成为提升用户体验的重要手段:保存用户设置、缓存API数据、记录浏览历史、离线功能支持等。但原生的localStorage API存在许多局限性:只能存储字符串、没有过期机制、容易出现类型转换错误、缺少批量操作等。今天我们就来打造一个功能完备的本地存储管理器,解决这些痛点问题。
想象你在开发一个电商网站的购物车功能:
用户添加商品到购物车 → 保存到本地存储
用户关闭浏览器 → 数据依然保留
用户第二天打开网站 → 购物车内容完好无损
但如果是临时优惠商品 → 需要在24小时后自动清除
这种场景需要既能持久保存数据,又能自动过期清理的存储机制。
在管理后台中保存用户的个性化配置:
用户设置 = {
theme: 'dark', // 主题偏好
language: 'zh-CN', // 语言设置
sidebarCollapsed: true, // 侧边栏状态
gridPageSize: 20, // 表格每页显示条数
recentSearches: ['关键词1', '关键词2'] // 最近搜索
}
这些设置需要:
为了提升应用性能,经常需要缓存API返回的数据:
缓存策略 = {
用户信息: 30分钟过期,
商品列表: 10分钟过期,
系统配置: 24小时过期,
临时数据: 5分钟过期
}
不同类型的数据需要不同的过期时间,过期后应该自动清理以节省存储空间。
// 原生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用户');
}
这种方式的问题:
// 手动实现过期功能,代码重复且容易出错
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;
}
// 每个功能都要重复写这样的代码...
// 难以管理存储空间
try {
localStorage.setItem('large-data', JSON.stringify(hugeObject));
} catch (e) {
if (e.name === 'QuotaExceededError') {
// 存储空间不足,但不知道如何清理
console.error('存储空间不足!');
// 手动清理?但清理哪些数据?
// localStorage.clear(); // 太粗暴,会删除所有数据
}
}
// 批量保存用户设置,需要多次调用
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') || '""');
// ... 代码重复且冗长
现在让我们来实现一个功能完备的存储管理器:
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}项` : ''}`
};
}
}
让我们看看这个存储管理器的基本使用:
// 创建存储管理器实例
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分钟后清理
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);
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分钟清理一次
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;
}
}
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("");
}
}
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);
}
});
}
}
通过我们自制的本地存储管理器,我们实现了:
核心优势:
强大功能:
实际应用场景:
性能和可靠性保障:
这个存储管理器不仅解决了原生localStorage的所有痛点,还提供了企业级应用所需的可靠性和性能保障。无论是简单的设置保存还是复杂的缓存管理,都能轻松胜任。
掌握了这个工具,你就能在项目中自信地处理任何客户端存储需求,让用户数据更安全、应用性能更优秀!
《JavaScript原生实战手册》专栏持续更新中,下期预告:《异步重试机制:网络请求的可靠性保障》