魯斯前端布魯斯前端

文章中英模式

常見的前端面試題目 - 用戶交互優化 快取策略

深入解析前端 runtime 時期的快取機制,包含 HTTP 快取、Web Storage 及離線策略,為面試提供完整的快取知識與實戰應用。

影片縮圖

懶得看文章?那就來看影片吧

基本概念

快取(Cache)是臨時存儲數據的技術,可以加速資源的獲取、減少不必要的網絡請求,並提升用戶體驗。在前端開發中,runtime 時期的快取策略是指在瀏覽器運行期間如何有效管理和使用臨時存儲的資源和數據。

使用者請求資源
檢查快取是否有所需資源
     ├─→ 有:直接從快取讀取 (Cache Hit)
     │    └─→ 返回資源,節省網絡請求
     └─→ 沒有:向遠端伺服器請求 (Cache Miss)
          └─→ 獲取資源 → 存入快取 → 返回資源

瀏覽器內建快取機制

HTTP 快取

瀏覽器的 HTTP 快取是最基本且強大的快取機制,主要通過 HTTP 標頭來控制。

常見的快取控制標頭:

Cache-Control: 設定快取行為
Expires: 設定資源過期時間
ETag: 資源版本標識符
Last-Modified: 資源最後修改時間

Cache-Control 詳解

Cache-Control 是 HTTP/1.1 中最重要的快取控制標頭,提供了多種指令:

指令說明
max-age資源在指定秒數內視為新鮮,可直接使用快取
no-cache必須先與服務器確認資源是否有更新,再使用快取
no-store完全不使用快取,每次都重新請求
public表示響應可被任何緩存區緩存,包括代理服務器
private表示響應只可被瀏覽器緩存,不可被中間代理緩存
must-revalidate過期後必須向源服務器驗證

no-cache 與 no-store 的區別

no-cache
  • 1. 不是「不快取」,而是「不直接使用快取」
  • 2. 資源會被存儲在快取中
  • 3. 使用前會發送條件請求(使用 ETag 或 Last-Modified)向服務器確認資源是否有更新
  • 4. 如未更新,服務器返回 304 狀態碼,瀏覽器使用快取
  • 5. 適用於:經常變化但大部分時間保持不變的內容(如新聞頁面)
no-store
  • 1. 完全不使用快取
  • 2. 不存儲任何響應
  • 3. 每次請求都從服務器獲取完整資源
  • 4. 適用於:敏感信息、持續變化的數據(如即時股票價格)
┌─────────────────────────────────────────────────────────────┐
│ no-cache vs no-store 工作流程                                │
└─────────────────────────────────────────────────────────────┘

【首次請求】
  Browser                  Server
     │                       │
     │─────── 請求資源 ─────>│
     │                       │
     │<── 返回資源+標頭 ────│
     │                       │

【再次請求 (no-cache)】
  Browser                  Server
     │                       │
     │── 條件請求(帶ETag) ─>│
     │                       │
     │<── 304 Not Modified ─│ (資源未變)
     │     或 200 (已變更)    │
     │                       │

【再次請求 (no-store)】
  Browser                  Server
     │                       │
     │─────── 請求資源 ─────>│
     │                       │
     │<── 返回完整資源 ────│ (總是返回完整資源)
     │                       │

ETag 工作機制

ETag 是服務器生成的資源版本標識符,用於檢測資源是否有變化:

  1. 1. 服務器返回資源時附帶 ETag 值(通常是內容的哈希值)
  2. 2. 瀏覽器下次請求相同資源時,發送 If-None-Match 標頭,值為之前收到的 ETag
  3. 3. 服務器比對 ETag:
    • 1. 若匹配(資源未變化):返回 304 狀態碼,不返回實體內容
    • 2. 若不匹配(資源已變化):返回 200 狀態碼和完整資源,以及新的 ETag
┌─────────────────────────────────────────────────────────────┐
│ ETag 工作流程                                                │
└─────────────────────────────────────────────────────────────┘

【首次請求】
Browser                                    Server
   │                                         │
   │────────── GET /image.jpg ───────────>  │
   │                                         │
   │<── HTTP/1.1 200 OK                      │
   │    ETag: "33a64df551425fcc55e4d42a"    │
   │    (圖片數據...)                         │
   │                                         │

【再次請求】
Browser                                     Server
   │                                          │
   │── GET /image.jpg                         │
   │   If-None-Match: "33a64df551425fcc55e4d42a" ─>│
   │                                          │ ┌─ 檢查ETag
   │                                          │ │  是否匹配
   │                                          │ ↓
   │<── HTTP/1.1 304 Not Modified            │ 匹配!
   │    ETag: "33a64df551425fcc55e4d42a"     │
   │    (無實體內容)                          │
   │                                          │
快取控制示例:
// 伺服器端設定快取標頭 (Node.js Express 範例)
app.get('/api/data', (req, res) => {
  // 設定快取一小時 (3600秒)
  res.setHeader('Cache-Control', 'max-age=3600');
  res.json({ data: 'cached content' });
});

// 前端請求時加入快取控制
fetch('/api/user', {
  cache: 'force-cache', // 強制使用快取
  // 或使用 'no-cache' 強制驗證,'no-store' 不使用快取
})

緩存控制策略圖:

┌─────────────────────────────────────────────────┐
│ 快取策略判斷流程                                  │
└─────────────────────────────────────────────────┘
                  請求資源
              檢查 Cache-Control
         ┌───────────┴───────────┐
         ↓                       ↓
   no-store存在                 檢查其他指令
  (完全不快取)                     │
         │             ┌─────────┴─────────┐
         │             ↓                   ↓
         │         max-age未過期          max-age已過期
         │        (直接使用快取)               │
         │             │                     │
         │             │                 有ETag/Last-Modified
         │             │                     │
         │             │                     ↓
         │             │               向伺服器發送條件請求
         │             │                     │
         │             │            ┌────────┴────────┐
         │             │            ↓                 ↓
         │             │      304 Not Modified   200 更新內容
         │             │      (繼續使用快取)     (更新快取並顯示)
         ↓             ↓            ↓                 ↓
     發起網絡請求  顯示快取內容    顯示快取內容       顯示新內容

本地存儲 (Web Storage)

除了 HTTP 快取,瀏覽器也提供了多種本地存儲機制,用於存儲和快取數據。

Web Storage 比較表

特性localStoragesessionStorageIndexedDBCookies
儲存容量~5MB~5MB無限制 (實際取決於硬碟空間)~4KB
生命週期永久存儲,除非手動刪除頁面會話結束後刪除永久存儲,除非手動刪除可設定過期時間
數據類型字符串字符串幾乎所有 JS 數據類型,包括二進制字符串
適用場景持久化配置、主題偏好表單數據暫存、頁面間傳值大量結構化數據、離線應用身份驗證、跟踪

Web Storage 使用示例

// localStorage 基本操作
localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme');
localStorage.removeItem('theme');

// sessionStorage 基本操作
sessionStorage.setItem('formData', JSON.stringify({ name: '張三' }));
const formData = JSON.parse(sessionStorage.getItem('formData'));

// 封裝 Storage 操作
class StorageUtil {
  static set(key, value) {
    localStorage.setItem(key, JSON.stringify(value));
  }
  
  static get(key) {
    const value = localStorage.getItem(key);
    return value ? JSON.parse(value) : null;
  }
  
  static remove(key) {
    localStorage.removeItem(key);
  }
  
  static clear() {
localStorage.clear();
  }
}

注意事項:

  • 1. Web Storage 只能存儲字符串,需要使用 JSON.stringify() 和 JSON.parse() 處理複雜數據
  • 2. 注意存儲容量限制,大型數據建議使用 IndexedDB
  • 3. 敏感數據不要存儲在本地,容易被攻擊者獲取

IndexedDB

IndexedDB 是一個低級 API,用於客戶端存儲大量結構化數據。它是一個事務型數據庫系統,類似於基於 SQL 的 RDBMS。

IndexedDB 基本用法

// 打開數據庫
const request = indexedDB.open('MyDatabase', 1);

// 處理數據庫升級
request.onupgradeneeded = (event) => {
  const db = event.target.result;
  // 創建對象倉庫(類似於表)
  const store = db.createObjectStore('users', { keyPath: 'id' });
  // 創建索引
  store.createIndex('name', 'name', { unique: false });
};

// 數據庫操作
request.onsuccess = (event) => {
  const db = event.target.result;
  
  // 寫入數據
  const transaction = db.transaction(['users'], 'readwrite');
  const store = transaction.objectStore('users');
  store.add({
    id: 1,
    name: '張三',
    email: 'zhangsan@example.com'
  });
  
  // 讀取數據
  const getRequest = store.get(1);
  getRequest.onsuccess = () => {
    console.log(getRequest.result);
  };
  
  // 使用索引查詢
  const nameIndex = store.index('name');
  const nameQuery = nameIndex.get('張三');
  nameQuery.onsuccess = () => {
    console.log(nameQuery.result);
  };
};

// 錯誤處理
request.onerror = (event) => {
  console.error('數據庫錯誤:', event.target.error);
};

IndexedDB 應用場景

  • 1. 離線應用數據存儲:保存應用數據,實現離線功能
  • 2. 大文件/二進制數據:存儲圖片、音頻等大型文件
  • 3. 結構化數據查詢:需要索引和複雜查詢的數據管理
  • 4. 客戶端緩存:緩存API響應,提升應用性能
注意事項
  • 1. IndexedDB 操作是異步的,需要正確處理回調或使用 Promise 封裝
  • 2. 注意瀏覽器的存儲限制,通常是可用磁盤空間的一定比例
  • 3. 實現數據同步機制,確保離線數據能夠與服務器同步

數據請求快取策略 (SWR)

SWR 是一種現代的數據獲取策略,先返回快取數據(stale),然後發送請求獲取最新數據(revalidate):

┌─────────────────────────────────────────────────┐
│ SWR (Stale-While-Revalidate) 請求流程            │
└─────────────────────────────────────────────────┘
                  組件渲染
              檢查快取是否有數據
         ┌───────────┴───────────┐
         ↓                       ↓
    有快取數據                 無快取數據
         │                       │
         ↓                       ↓
    立即返回快取數據          顯示載入狀態
         │                       │
         │                       │
         │                       │
         └───────────┬───────────┘
              發起網絡請求獲取新數據
         ┌───────────┴───────────┐
         ↓                       ↓
    請求成功                   請求失敗
         │                       │
         ↓                       ↓
    更新快取數據              顯示錯誤狀態
    使用新數據更新UI              
                     
              自動重新驗證觸發條件:
              - 窗口聚焦時
              - 網絡重連時
              - 定時刷新
如何解決前端快取更新的問題?

前端快取更新是一個常見的挑戰,有以下幾種解決方案:

  1. 1. 使用版本號或哈希值
    <script src="app.3f8a7b9c.js"></script>
    
    <script src="app.js?v=1.2.3"></script>
  2. 2. 設置合適的快取控制標頭
    // HTML - 不快取或快取時間短
    Cache-Control: no-cache, must-revalidate
    
    // CSS/JS - 長時間快取,但使用版本化URL
    Cache-Control: max-age=31536000, immutable
  3. 3. Service Worker 版本管理
    // sw.js
    const CACHE_VERSION = 'v1.2.3';
    const CACHE_NAME = `app-shell-${CACHE_VERSION}`;
    
    self.addEventListener('activate', event => {
      event.waitUntil(
        caches.keys().then(cacheNames => {
          return Promise.all(
            cacheNames
              .filter(name => name.startsWith('app-shell-'))
              .filter(name => name !== CACHE_NAME)
              .map(name => caches.delete(name))
          );
        })
      );
    });
  4. 4. API 數據的刷新策略
    // SWR 策略:先顯示快取數據,然後在背景刷新
    function fetchData(url) {
      // 從快取獲取數據並立即顯示
      const cachedData = localStorage.getItem(url);
      if (cachedData) {
        renderData(JSON.parse(cachedData));
      }
      
      // 同時發起新請求獲取最新數據
      fetch(url)
        .then(res => res.json())
        .then(newData => {
          // 更新快取
          localStorage.setItem(url, JSON.stringify(newData));
          // 更新顯示
          renderData(newData);
        });
    }
  5. 5. 手動強制更新
    // 提供用戶手動刷新功能
    function forceRefresh() {
      // 清除特定數據快取
      localStorage.removeItem('user-data');
      
      // 或者重新載入頁面,繞過快取
      location.reload(true);
    }

實際應用場景

┌─────────────────────────────────────────────────┐
│ 電商網站商品頁面的快取策略                        │
└─────────────────────────────────────────────────┘
  
  1. 靜態資源 (CSS/JS/圖片)
     ├─→ HTTP 快取: Cache-Control: max-age=86400 (一天)
     └─→ Service Worker: 預快取核心資源,支持離線訪問
  
  2. 商品基本信息
     ├─→ localStorage: 最近瀏覽的20件商品
     └─→ SWR: 顯示快取資料同時背景重新驗證
     
  3. 用戶個人化內容 (購物車/收藏)
     ├─→ 短期快取: Cache-Control: max-age=60 (一分鐘)
     
  4. 搜索結果/推薦
     └─→ sessionStorage: 存儲本次會話內的搜索結果

🔥 常見面試題目

(一)有哪些HTTP 快取策略?「強快取」和「協商快取」是什麼?

解答:

強快取(Strong Cache)和協商快取(Negotiation Cache)是 HTTP 快取的兩種主要機制:

強快取
  • 1. 直接使用本地快取,不與伺服器通訊
  • 2. 主要通過 Cache-Control Expires 標頭控制
  • 3. 響應速度最快,減輕伺服器負擔
  • 4. 例如:Cache-Control: max-age=3600(快取1小時內有效)
協商快取
  • 1. 瀏覽器先詢問伺服器資源是否變更
  • 2. 使用 ETag Last-Modified 標頭與服務器協商
  • 3. 如未變更,服務器返回 304 Not Modified,瀏覽器使用本地快取
  • 4. 如已變更,服務器返回 200 OK 和完整資源
  • 5. 例如:瀏覽器發送 If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

(二)請解釋 Cache-Control 中 no-cache 和 no-store 的區別

解答:

實際應用決策:

  • 1. 若數據經常變化,但仍希望利用快取減輕服務器負載:使用 no-cache
  • 2. 若數據非常敏感或每次必須顯示最新數據:使用 no-store

(三)如何實現前端應用的離線功能?

解答:

實現前端應用離線功能主要依靠 Service Worker 和各種存儲 API:

  1. 1. Service Worker 註冊與安裝
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/sw.js')
        .then(reg => console.log('SW registered'))
        .catch(err => console.error('SW registration failed', err));
    }
  2. 2. 預快取核心資源
    // sw.js
    self.addEventListener('install', event => {
      event.waitUntil(
        caches.open('app-shell').then(cache => {
          return cache.addAll([
            '/',
            '/index.html',
            '/styles.css',
            '/app.js',
            '/offline.html'
          ]);
        })
      );
    });
  3. 3. 離線響應策略
    self.addEventListener('fetch', event => {
      event.respondWith(
        caches.match(event.request)
          .then(response => {
            // 快取優先,網絡作為後備方案
            return response || fetch(event.request)
              .then(response => {
                return caches.open('dynamic-content')
                  .then(cache => {
                    cache.put(event.request.url, response.clone());
                    return response;
                  });
              })
              .catch(() => {
                // 如果是頁面請求,返回離線頁面
                if (event.request.mode === 'navigate') {
                  return caches.match('/offline.html');
                }
              });
          })
      );
    });
  4. 4. 數據同步
    // 使用 IndexedDB 存儲離線操作
    // 當網絡恢復時同步數據
    self.addEventListener('sync', event => {
      if (event.tag === 'sync-messages') {
        event.waitUntil(syncMessages());
      }
    });
    
    function syncMessages() {
      // 獲取離線時存儲的消息並發送到服務器
      return getMessagesFromIndexedDB()
        .then(messages => {
          return Promise.all(messages.map(msg => {
            return fetch('/api/messages', {
              method: 'POST',
              body: JSON.stringify(msg)
            }).then(() => {
              // 發送成功後從 IndexedDB 中刪除
              return deleteMessageFromIndexedDB(msg.id);
            });
          }));
        });
    }

(四)如何選擇適合的前端存儲方案?各自有什麼優缺點?

解答:

選擇前端存儲方案時應考慮數據特性、使用場景與安全性需求:

1. Cookies
  • 1. 優點:可設置過期時間、自動隨請求發送到伺服器
  • 2. 缺點:容量小(約4KB)、增加網路傳輸量、需小心處理安全問題
  • 3. 適用場景:身份驗證令牌、跨頁面追踪
2. localStorage
  • 1. 優點:永久存儲、跨頁面共享、容量較大(~5MB)、使用簡單
  • 2. 缺點:同步操作可能阻塞UI、只能存儲字符串、無法設置過期時間
  • 3. 適用場景:用戶偏好設定、長期保存的應用狀態
3. sessionStorage
  • 1. 優點:頁面會話期間有效、頁面重新載入後仍存在、相對安全
  • 2. 缺點:同步操作、只能存儲字符串、不同頁面無法共享
  • 3. 適用場景:表單暫存數據、單次會話的狀態保存
4. IndexedDB
  • 1. 優點:大容量存儲、支持索引和查詢、異步操作、事務支持
  • 2. 缺點:API複雜、學習曲線陡峭、舊版瀏覽器支持有限
  • 3. 適用場景:大量結構化數據存儲、離線應用、複雜數據操作
5. Cache API
  • 1. 優點:專為HTTP請求/響應設計、可與Service Worker結合使用
  • 2. 缺點:主要用於HTTP快取、非通用數據存儲
  • 3. 適用場景:離線應用、PWA、資源快取

決策準則:

  • 1. 量少且需跨請求/身份驗證:使用 Cookies
  • 2. 簡單配置/UI狀態持久保存:使用 localStorage
  • 3. 單次會話暫存數據:使用 sessionStorage
  • 4. 大量複雜數據或需要查詢能力:使用 IndexedDB
  • 5. 需要離線工作或資源快取:使用 Cache API + Service Worker

實際應用往往會組合不同存儲方式:例如離線應用可能同時使用 IndexedDB(核心數據)、localStorage(用戶設置)和 Cache API(資源快取)。

(五)如何解決前端快取更新的問題?

解答:

前端快取更新是一個常見的挑戰,有以下幾種解決方案:

  1. 1. 使用版本號或哈希值
    <script src="app.3f8a7b9c.js"></script>
    
    <script src="app.js?v=1.2.3"></script>
  2. 2. 設置合適的快取控制標頭
    // HTML - 不快取或快取時間短
    Cache-Control: no-cache, must-revalidate
    
    // CSS/JS - 長時間快取,但使用版本化URL
    Cache-Control: max-age=31536000, immutable
  3. 3. Service Worker 版本管理
    // sw.js
    const CACHE_VERSION = 'v1.2.3';
    const CACHE_NAME = `app-shell-${CACHE_VERSION}`;
    
    self.addEventListener('activate', event => {
      event.waitUntil(
        caches.keys().then(cacheNames => {
          return Promise.all(
            cacheNames
              .filter(name => name.startsWith('app-shell-'))
              .filter(name => name !== CACHE_NAME)
              .map(name => caches.delete(name))
          );
        })
      );
    });
  4. 4. API 數據的刷新策略
    // SWR 策略:先顯示快取數據,然後在背景刷新
    function fetchData(url) {
      // 從快取獲取數據並立即顯示
      const cachedData = localStorage.getItem(url);
      if (cachedData) {
        renderData(JSON.parse(cachedData));
      }
      
      // 同時發起新請求獲取最新數據
      fetch(url)
        .then(res => res.json())
        .then(newData => {
          // 更新快取
          localStorage.setItem(url, JSON.stringify(newData));
          // 更新顯示
          renderData(newData);
        });
    }
  5. 5. 手動強制更新
    // 提供用戶手動刷新功能
    function forceRefresh() {
      // 清除特定數據快取
      localStorage.removeItem('user-data');
      
      // 或者重新載入頁面,繞過快取
      location.reload(true);
    }