你是不是也遇到过这样的场景?
用户刚填完一个超长的表单,不小心刷新了页面,所有数据都没了... 从接口请求的数据,用户每次操作都要重新加载,体验卡成PPT... 应用离线状态下完全无法使用,用户直接流失...
别担心!今天我就带你彻底解决这些问题。看完这篇文章,你将掌握一套完整的数据交互方案,让你的应用在任何网络状态下都能流畅运行。
为什么数据存储这么重要?
想象一下,你去超市购物,每次想买什么东西,都要跑回家查一下购物清单,然后再跑回超市... 这得多累啊!
网页应用也是同样的道理。合理的数据存储就像你的购物清单,把需要的东西记下来,随用随取,效率直接翻倍。
先来看看我们最常用的数据获取方式------Fetch API
Fetch API:现代前端的数据搬运工
Fetch API 是现在最主流的数据请求方式,比老旧的 XMLHttpRequest 好用太多了。它基于 Promise,写起来特别优雅。
javascript
// 最基本的 GET 请求
async function fetchUserData(userId) {
try {
// 发起请求,等待响应
const response = await fetch(`https://api.example.com/users/${userId}`);
// 检查响应是否成功(状态码 200-299)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 解析 JSON 数据
const userData = await response.json();
return userData;
} catch (error) {
// 统一的错误处理
console.error('获取用户数据失败:', error);
throw error;
}
}
// 使用示例
fetchUserData(123)
.then(user => {
console.log('用户信息:', user);
// 在这里更新页面显示
})
.catch(error => {
// 显示错误提示给用户
alert('加载用户信息失败,请重试');
});
但光会请求数据还不够,聪明的开发者都知道:好的数据要懂得缓存。这就引出了我们的主角------本地存储。
本地存储三剑客:sessionStorage、localStorage、IndexedDB
1. sessionStorage:短暂的记忆
sessionStorage 就像你的短期记忆,页面会话结束时数据就清空了。适合存储一些临时数据。
javascript
// 保存表单草稿
function saveFormDraft(formData) {
// 将对象转换为 JSON 字符串存储
sessionStorage.setItem('formDraft', JSON.stringify(formData));
console.log('表单草稿已保存');
}
// 读取表单草稿
function loadFormDraft() {
const draft = sessionStorage.getItem('formDraft');
if (draft) {
// 将 JSON 字符串解析回对象
return JSON.parse(draft);
}
return null;
}
// 清除草稿
function clearFormDraft() {
sessionStorage.removeItem('formDraft');
console.log('表单草稿已清除');
}
// 使用示例:页面加载时恢复草稿
window.addEventListener('load', () => {
const draft = loadFormDraft();
if (draft) {
// 用草稿数据填充表单
document.getElementById('username').value = draft.username || '';
document.getElementById('email').value = draft.email || '';
console.log('表单草稿已恢复');
}
});
// 输入时实时保存
document.getElementById('myForm').addEventListener('input', (event) => {
const formData = {
username: document.getElementById('username').value,
email: document.getElementById('email').value
};
saveFormDraft(formData);
});
2. localStorage:持久的仓库
localStorage 是长期存储,除非主动清除,否则数据会一直存在。适合存储用户偏好设置等。
javascript
// 用户主题偏好管理
class ThemeManager {
constructor() {
this.currentTheme = this.getSavedTheme() || 'light';
this.applyTheme(this.currentTheme);
}
// 获取保存的主题
getSavedTheme() {
return localStorage.getItem('userTheme');
}
// 保存主题偏好
saveTheme(theme) {
localStorage.setItem('userTheme', theme);
this.currentTheme = theme;
console.log(`主题已保存: ${theme}`);
}
// 应用主题
applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
this.saveTheme(theme);
}
// 切换主题
toggleTheme() {
const newTheme = this.currentTheme === 'light' ? 'dark' : 'light';
this.applyTheme(newTheme);
}
// 清除主题设置
clearTheme() {
localStorage.removeItem('userTheme');
this.currentTheme = 'light';
this.applyTheme('light');
console.log('主题设置已清除');
}
}
// 使用示例
const themeManager = new ThemeManager();
// 主题切换按钮
document.getElementById('themeToggle').addEventListener('click', () => {
themeManager.toggleTheme();
});
3. IndexedDB:大数据专家
当你的数据量很大,或者需要复杂查询时,IndexedDB 就是最佳选择。
javascript
// 创建一个简单的数据库管理器
class DBManager {
constructor(dbName, version) {
this.dbName = dbName;
this.version = version;
this.db = null;
}
// 打开数据库
async open() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};
// 第一次创建数据库时初始化结构
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 创建用户表
if (!db.objectStoreNames.contains('users')) {
const store = db.createObjectStore('users', { keyPath: 'id' });
// 创建索引,方便按姓名搜索
store.createIndex('name', 'name', { unique: false });
}
// 创建文章表
if (!db.objectStoreNames.contains('articles')) {
const store = db.createObjectStore('articles', { keyPath: 'id' });
store.createIndex('title', 'title', { unique: false });
store.createIndex('createdAt', 'createdAt', { unique: false });
}
};
});
}
// 添加数据
async add(storeName, data) {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.add(data);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
// 获取所有数据
async getAll(storeName) {
const transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
// 按索引查询
async getByIndex(storeName, indexName, value) {
const transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const index = store.index(indexName);
return new Promise((resolve, reject) => {
const request = index.getAll(value);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
}
// 使用示例
async function initDB() {
const dbManager = new DBManager('MyAppDB', 1);
await dbManager.open();
// 添加示例用户
await dbManager.add('users', {
id: 1,
name: '张三',
email: 'zhangsan@example.com',
createdAt: new Date()
});
// 获取所有用户
const users = await dbManager.getAll('users');
console.log('所有用户:', users);
return dbManager;
}
// 初始化数据库
initDB().then(dbManager => {
console.log('数据库初始化完成');
});
实战:构建智能数据缓存系统
现在让我们把 Fetch API 和本地存储结合起来,打造一个真正智能的数据缓存系统。
javascript
// 智能数据管理器
class SmartDataManager {
constructor() {
this.cache = new Map(); // 内存缓存
}
// 获取数据(带缓存)
async getData(url, options = {}) {
const {
cacheKey = url, // 缓存键名
cacheTime = 5 * 60 * 1000, // 默认缓存5分钟
forceRefresh = false // 强制刷新
} = options;
// 检查内存缓存
if (!forceRefresh) {
const cached = this.getFromCache(cacheKey, cacheTime);
if (cached) {
console.log('从内存缓存返回数据');
return cached;
}
// 检查 localStorage 缓存
const stored = this.getFromStorage(cacheKey, cacheTime);
if (stored) {
console.log('从本地存储返回数据');
// 同时更新内存缓存
this.cache.set(cacheKey, {
data: stored,
timestamp: Date.now()
});
return stored;
}
}
// 缓存中没有,从接口获取
console.log('从接口获取数据');
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
// 同时更新内存缓存和本地存储
this.setCache(cacheKey, data);
this.setStorage(cacheKey, data);
return data;
} catch (error) {
console.error('获取数据失败:', error);
throw error;
}
}
// 从内存缓存获取
getFromCache(key, cacheTime) {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < cacheTime) {
return cached.data;
}
return null;
}
// 从本地存储获取
getFromStorage(key, cacheTime) {
try {
const stored = localStorage.getItem(`cache_${key}`);
if (stored) {
const { data, timestamp } = JSON.parse(stored);
if (Date.now() - timestamp < cacheTime) {
return data;
} else {
// 缓存过期,清理
localStorage.removeItem(`cache_${key}`);
}
}
} catch (error) {
console.warn('读取缓存失败:', error);
}
return null;
}
// 设置内存缓存
setCache(key, data) {
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
// 设置本地存储
setStorage(key, data) {
try {
localStorage.setItem(`cache_${key}`, JSON.stringify({
data,
timestamp: Date.now()
}));
} catch (error) {
console.warn('存储缓存失败:', error);
// 如果存储失败(比如超出容量),清理最旧的缓存
this.cleanupStorage();
}
}
// 清理过期缓存
cleanupStorage() {
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith('cache_')) {
try {
const stored = JSON.parse(localStorage.getItem(key));
// 删除超过1天的缓存
if (Date.now() - stored.timestamp > 24 * 60 * 60 * 1000) {
keysToRemove.push(key);
}
} catch (error) {
// 数据格式错误,直接删除
keysToRemove.push(key);
}
}
}
keysToRemove.forEach(key => localStorage.removeItem(key));
}
// 清除指定缓存
clearCache(key) {
this.cache.delete(key);
localStorage.removeItem(`cache_${key}`);
}
// 清除所有缓存
clearAllCache() {
this.cache.clear();
Object.keys(localStorage)
.filter(key => key.startsWith('cache_'))
.forEach(key => localStorage.removeItem(key));
}
}
// 使用示例
const dataManager = new SmartDataManager();
// 获取用户列表(带缓存)
async function loadUsers() {
try {
const users = await dataManager.getData('/api/users', {
cacheKey: 'user_list',
cacheTime: 10 * 60 * 1000 // 缓存10分钟
});
// 渲染用户列表
renderUserList(users);
} catch (error) {
// 显示错误状态
showError('加载用户列表失败');
}
}
// 强制刷新数据
async function refreshUsers() {
try {
const users = await dataManager.getData('/api/users', {
cacheKey: 'user_list',
forceRefresh: true // 强制从接口获取最新数据
});
renderUserList(users);
showSuccess('数据已刷新');
} catch (error) {
showError('刷新数据失败');
}
}
离线优先:打造极致用户体验
现代 Web 应用应该具备离线能力,让用户在网络不稳定时也能正常使用。
javascript
// 离线优先的数据同步器
class OfflineFirstSync {
constructor() {
this.dbManager = null;
this.pendingSync = []; // 待同步的操作
this.init();
}
async init() {
// 初始化 IndexedDB
this.dbManager = new DBManager('OfflineApp', 1);
await this.dbManager.open();
// 监听网络状态
this.setupNetworkListener();
// 尝试同步待处理的操作
this.trySyncPending();
}
// 设置网络状态监听
setupNetworkListener() {
window.addEventListener('online', () => {
console.log('网络已连接,开始同步数据...');
this.trySyncPending();
});
window.addEventListener('offline', () => {
console.log('网络已断开,进入离线模式');
this.showOfflineIndicator();
});
}
// 创建数据(离线优先)
async createData(storeName, data) {
// 先保存到本地数据库
const localId = await this.dbManager.add(storeName, {
...data,
_local: true, // 标记为本地创建
_synced: false, // 未同步
_createdAt: new Date()
});
// 添加到待同步队列
this.pendingSync.push({
type: 'create',
storeName,
data: { ...data, _localId: localId }
});
// 尝试立即同步
await this.trySyncPending();
return localId;
}
// 尝试同步待处理操作
async trySyncPending() {
if (!navigator.onLine || this.pendingSync.length === 0) {
return;
}
console.log(`开始同步 ${this.pendingSync.length} 个操作`);
const successes = [];
const failures = [];
for (const operation of [...this.pendingSync]) {
try {
await this.syncOperation(operation);
successes.push(operation);
// 从待同步队列中移除成功的操作
const index = this.pendingSync.indexOf(operation);
if (index > -1) {
this.pendingSync.splice(index, 1);
}
} catch (error) {
console.error('同步操作失败:', error);
failures.push(operation);
}
}
if (successes.length > 0) {
console.log(`成功同步 ${successes.length} 个操作`);
this.showSyncSuccess(successes.length);
}
if (failures.length > 0) {
console.warn(`${failures.length} 个操作同步失败,将在下次重试`);
}
}
// 同步单个操作
async syncOperation(operation) {
switch (operation.type) {
case 'create':
// 调用 API 创建数据
const response = await fetch('/api/' + operation.storeName, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(operation.data)
});
if (!response.ok) {
throw new Error(`创建失败: ${response.status}`);
}
const result = await response.json();
// 更新本地数据,标记为已同步
// 这里可以根据需要更新本地记录的ID等
console.log('数据同步成功:', result);
break;
default:
console.warn('未知的操作类型:', operation.type);
}
}
// 显示离线指示器
showOfflineIndicator() {
// 在实际应用中,可以显示一个离线提示条
const indicator = document.createElement('div');
indicator.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
background: #ff6b6b;
color: white;
text-align: center;
padding: 10px;
z-index: 1000;
`;
indicator.textContent = '当前处于离线模式,部分功能可能受限';
indicator.id = 'offline-indicator';
document.body.appendChild(indicator);
}
// 显示同步成功提示
showSyncSuccess(count) {
const indicator = document.getElementById('offline-indicator');
if (indicator) {
indicator.remove();
}
// 显示同步成功提示(可以替换为更优雅的通知)
console.log(`成功同步 ${count} 条数据`);
}
// 获取数据(离线优先)
async getData(storeName, useLocalFirst = true) {
if (useLocalFirst) {
// 先返回本地数据
const localData = await this.dbManager.getAll(storeName);
// 同时在后台尝试获取最新数据
this.fetchLatestData(storeName);
return localData;
} else {
// 直接获取最新数据
return await this.fetchLatestData(storeName);
}
}
// 获取最新数据
async fetchLatestData(storeName) {
if (!navigator.onLine) {
throw new Error('网络不可用');
}
try {
const response = await fetch(`/api/${storeName}`);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
// 更新本地数据库
// 这里需要根据具体业务逻辑实现数据合并
console.log('获取到最新数据:', data);
return data;
} catch (error) {
console.error('获取最新数据失败:', error);
throw error;
}
}
}
// 使用示例
const offlineSync = new OfflineFirstSync();
// 在离线状态下创建用户
async function createUserOffline(userData) {
try {
const localId = await offlineSync.createData('users', userData);
console.log('用户已创建(本地):', localId);
showSuccess('用户已保存,将在网络恢复后同步');
} catch (error) {
console.error('创建用户失败:', error);
showError('保存用户失败');
}
}
性能优化与最佳实践
掌握了基础用法,再来看看一些提升性能的实用技巧。
javascript
// 防抖请求,避免频繁调用接口
function createDebouncedFetcher(delay = 500) {
let timeoutId;
return async function debouncedFetch(url, options) {
// 清除之前的定时器
if (timeoutId) {
clearTimeout(timeoutId);
}
// 设置新的定时器
return new Promise((resolve, reject) => {
timeoutId = setTimeout(async () => {
try {
const response = await fetch(url, options);
const data = await response.json();
resolve(data);
} catch (error) {
reject(error);
}
}, delay);
});
};
}
// 使用防抖的搜索功能
const debouncedSearch = createDebouncedFetcher(300);
document.getElementById('searchInput').addEventListener('input', async (event) => {
const query = event.target.value.trim();
if (query.length < 2) {
// 清空搜索结果
clearSearchResults();
return;
}
try {
const results = await debouncedSearch(`/api/search?q=${encodeURIComponent(query)}`);
displaySearchResults(results);
} catch (error) {
console.error('搜索失败:', error);
// 可以显示本地缓存的结果或错误提示
}
});
// 批量操作优化
async function batchOperations(operations, batchSize = 5) {
const results = [];
for (let i = 0; i < operations.length; i += batchSize) {
const batch = operations.slice(i, i + batchSize);
// 并行执行批次内的操作
const batchResults = await Promise.allSettled(
batch.map(op => executeOperation(op))
);
results.push(...batchResults);
// 可选:批次间延迟,避免对服务器造成太大压力
if (i + batchSize < operations.length) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
return results;
}
// 数据压缩,减少存储空间
function compressData(data) {
// 简单的数据压缩示例
const compressed = {
// 移除空值
...Object.fromEntries(
Object.entries(data).filter(([_, value]) =>
value !== null && value !== undefined && value !== ''
)
),
// 添加压缩标记
_compressed: true
};
return compressed;
}
// 数据解压缩
function decompressData(compressedData) {
const { _compressed, ...data } = compressedData;
return data;
}
// 使用压缩存储
function saveCompressedData(key, data) {
const compressed = compressData(data);
localStorage.setItem(key, JSON.stringify(compressed));
}
function loadCompressedData(key) {
const stored = localStorage.getItem(key);
if (stored) {
const compressed = JSON.parse(stored);
return decompressData(compressed);
}
return null;
}
错误处理与监控
健壮的应用离不开完善的错误处理。
javascript
// 增强的错误处理包装器
function createRobustFetcher(options = {}) {
const {
maxRetries = 3,
retryDelay = 1000,
timeout = 10000
} = options;
return async function robustFetch(url, fetchOptions = {}) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// 创建超时控制器
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const response = await fetch(url, {
...fetchOptions,
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
lastError = error;
console.warn(`请求失败 (尝试 ${attempt}/${maxRetries}):`, error);
if (attempt < maxRetries) {
// 指数退避延迟
const delay = retryDelay * Math.pow(2, attempt - 1);
console.log(`等待 ${delay}ms 后重试...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// 所有重试都失败了
throw new Error(`请求失败,已重试 ${maxRetries} 次: ${lastError.message}`);
};
}
// 使用增强的请求器
const robustFetch = createRobustFetcher({
maxRetries: 3,
retryDelay: 1000,
timeout: 15000
});
// 数据健康检查
class DataHealthChecker {
static checkLocalStorage() {
const issues = [];
try {
// 测试写入和读取
const testKey = '__health_check__';
const testValue = { timestamp: Date.now() };
localStorage.setItem(testKey, JSON.stringify(testValue));
const retrieved = JSON.parse(localStorage.getItem(testKey));
localStorage.removeItem(testKey);
if (!retrieved || retrieved.timestamp !== testValue.timestamp) {
issues.push('localStorage 数据完整性检查失败');
}
} catch (error) {
issues.push(`localStorage 不可用: ${error.message}`);
}
return issues;
}
static checkIndexedDB() {
return new Promise((resolve) => {
const issues = [];
const request = indexedDB.open('health_check', 1);
request.onerror = () => {
issues.push('IndexedDB 无法打开');
resolve(issues);
};
request.onsuccess = () => {
const db = request.result;
db.close();
// 清理测试数据库
indexedDB.deleteDatabase('health_check');
resolve(issues);
};
request.onblocked = () => {
issues.push('IndexedDB 被阻塞');
resolve(issues);
};
});
}
static async runAllChecks() {
const localStorageIssues = this.checkLocalStorage();
const indexedDBIssues = await this.checkIndexedDB();
const allIssues = [...localStorageIssues, ...indexedDBIssues];
if (allIssues.length === 0) {
console.log('✅ 所有存储系统正常');
} else {
console.warn('❌ 存储系统问题:', allIssues);
}
return allIssues;
}
}
// 定期运行健康检查
setInterval(async () => {
await DataHealthChecker.runAllChecks();
}, 5 * 60 * 1000); // 每5分钟检查一次
总结
通过今天的学习,相信你已经掌握了:
✅ Fetch API 的现代用法和错误处理 ✅ 三种本地存储方案的适用场景 ✅ 如何构建智能缓存系统提升性能 ✅ 离线优先的设计思路 ✅ 各种性能优化和监控技巧
数据交互不再是简单的"请求-显示",而是要考虑缓存、离线、同步、性能等方方面面。一个好的数据层设计,能让你的应用用户体验提升好几个档次。