你是不是也遇到過這樣的場景?
使用者剛填完一個超長的表單,不小心刷新了頁面,所有資料都沒了...
從介面請求的資料,使用者每次操作都要重新載入,體驗卡成PPT...
應用離線狀態下完全無法使用,使用者直接流失...
別擔心!今天我就帶你徹底解決這些問題。看完這篇文章,你將掌握一套完整的資料互動方案,讓你的應用在任何網路狀態下都能流暢運行。
想像一下,你去超市購物,每次想買什麼東西,都要跑回家查一下購物清單,然後再跑回超市... 這得多累啊!
網頁應用也是同樣的道理。合理的資料存儲就像你的購物清單,把需要的東西記下來,隨用隨取,效率直接翻倍。
先來看看我們最常用的資料獲取方式——Fetch API
Fetch API 是現在最主流的資料請求方式,比老舊的 XMLHttpRequest 好用太多了。它基於 Promise,寫起來特別優雅。
// 最基本的 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 就像你的短期記憶,頁面會話結束時資料就清空了。適合存儲一些臨時資料。
// 保存表單草稿
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);
});
localStorage 是長期存儲,除非主動清除,否則資料會一直存在。適合存儲使用者偏好設定等。
// 使用者主題偏好管理
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();
});
當你的資料量很大,或者需要複雜查詢時,IndexedDB 就是最佳選擇。
// 創建一個簡單的資料庫管理器
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: '[email protected]',
createdAt: new Date()
});
// 獲取所有使用者
const users = await dbManager.getAll('users');
console.log('所有使用者:', users);
return dbManager;
}
// 初始化資料庫
initDB().then(dbManager => {
console.log('資料庫初始化完成');
});
現在讓我們把 Fetch API 和本地存儲結合起來,打造一個真正智能的資料快取系統。
// 智能資料管理器
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 應用應該具備離線能力,讓使用者在網路不穩定時也能正常使用。
// 離線優先的資料同步器
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();
// 更新本地資料,標記為已同步
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('保存使用者失敗');
}
}
掌握了基礎用法,再來看看一些提升性能的實用技巧。
// 防抖請求,避免頻繁呼叫介面
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;
}
健壯的應用離不了完善的錯誤處理。
// 增強的錯誤處理包裝器
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 的現代用法和錯誤處理
✅ 三種本地存儲方案的適用場景
✅ 如何構建智能快取系統提升性能
✅ 離線優先的設計思路
✅ 各種性能優化和監控技巧
資料互動不再是簡單的"請求-顯示",而是要考慮快取、離線、同步、性能等方方面面。一個好的資料層設計,能讓你的應用使用者體驗提升好幾檔次。