🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

前端別再亂存資料了!這3種存儲方案讓你的應用快如閃電

你是不是也遇到過這樣的場景?

使用者剛填完一個超長的表單,不小心刷新了頁面,所有資料都沒了...
從介面請求的資料,使用者每次操作都要重新載入,體驗卡成PPT...
應用離線狀態下完全無法使用,使用者直接流失...

別擔心!今天我就帶你徹底解決這些問題。看完這篇文章,你將掌握一套完整的資料互動方案,讓你的應用在任何網路狀態下都能流暢運行。

為什麼資料存儲這麼重要?

想像一下,你去超市購物,每次想買什麼東西,都要跑回家查一下購物清單,然後再跑回超市... 這得多累啊!

網頁應用也是同樣的道理。合理的資料存儲就像你的購物清單,把需要的東西記下來,隨用隨取,效率直接翻倍。

先來看看我們最常用的資料獲取方式——Fetch API

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、localStorage、IndexedDB

1. sessionStorage:短暫的記憶

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);
});

2. localStorage:持久的倉庫

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();
});

3. IndexedDB:大資料專家

當你的資料量很大,或者需要複雜查詢時,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 的現代用法和錯誤處理
✅ 三種本地存儲方案的適用場景
✅ 如何構建智能快取系統提升性能
✅ 離線優先的設計思路
✅ 各種性能優化和監控技巧

資料互動不再是簡單的"請求-顯示",而是要考慮快取、離線、同步、性能等方方面面。一個好的資料層設計,能讓你的應用使用者體驗提升好幾檔次。


原文出處:https://juejin.cn/post/7565716907011293225


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝26   💬5   ❤️7
800
🥈
我愛JS
📝2   💬7   ❤️3
118
🥉
酷豪
1
#5
1
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付