iT邦幫忙

2025 iThome 鐵人賽

DAY 0
0

系列文章: 前端工程師的 Modern Web 實踐之道 - Day 11
預計閱讀時間: 12 分鐘
難度等級: ⭐⭐⭐⭐☆

🎯 今日目標

在前一篇文章中,我們探討了狀態管理的不同方案選擇。今天我們將深入探討前端開發中另一個核心課題:API 設計與前端整合。這個主題將幫助你從全端視角理解如何設計高效能、易維護的 API,並在前端實作最佳的資料處理策略。

為什麼要關注這個主題?

  • 降低溝通成本: 良好的 API 設計能減少前後端團隊 70% 的溝通時間
  • 提升開發效率: 合理的資料結構能讓前端開發速度提升 50%
  • 改善使用者體驗: 最佳化的 API 呼叫和快取策略直接影響頁面載入速度
  • 技術演進趨勢: 從 RESTful 到 GraphQL,理解不同方案的適用場景是現代前端工程師的必備技能

還在為過度請求(Over-fetching)或請求不足(Under-fetching)而煩惱嗎?每次新增一個頁面就要請後端加一個新 API?今天我們來聊聊如何徹底解決這些問題。

🔍 深度分析:API 設計的技術演進

問題背景與現狀

在現代 Web 應用中,前後端分離已經成為標準架構。但這也帶來新的挑戰:

傳統 RESTful API 的痛點:

// 取得使用者資訊需要多次請求
const user = await fetch('/api/users/123');
const posts = await fetch('/api/users/123/posts');
const comments = await fetch('/api/users/123/comments');
const followers = await fetch('/api/users/123/followers');

// 4 次請求,4 次往返,大量時間浪費
// 更糟的是:可能拿到很多用不到的資料

GraphQL 的承諾:

// 一次請求拿到所有需要的資料
const data = await graphql(`
  query {
    user(id: "123") {
      name
      email
      posts(limit: 10) {
        title
        createdAt
      }
      followers(limit: 5) {
        name
      }
    }
  }
`);

// 只拿需要的欄位,一次請求完成

但 GraphQL 真的是銀彈嗎?讓我們深入分析。

技術方案深入解析

RESTful API:成熟穩定的經典方案

核心特性:

  1. 資源導向: 每個 URL 代表一個資源
  2. HTTP 語意: 使用 GET、POST、PUT、DELETE 對應 CRUD 操作
  3. 無狀態: 每個請求獨立,伺服器不保存客戶端狀態
  4. 快取友善: 可以充分利用 HTTP 快取機制

適用場景:

  • 資料結構穩定,變化不頻繁的應用
  • 需要充分利用 CDN 和 HTTP 快取的場景
  • 團隊對 REST 熟悉,不想引入新的學習成本
  • 公開 API,需要簡單易用的介面

GraphQL:靈活強大的現代選擇

核心特性:

  1. 精確資料獲取: 客戶端決定需要哪些欄位
  2. 單一端點: 所有請求通過一個 URL
  3. 強型別系統: Schema 定義清晰的資料結構
  4. 即時查詢: 支援訂閱(Subscription)機制

適用場景:

  • 資料關聯複雜,頻繁需要組合不同資源
  • 多平台應用,不同客戶端需要不同資料
  • 快速迭代的產品,API 需求變化頻繁
  • 行動應用,需要最小化網路請求

實際專案的選擇策略

讓我分享一個真實案例:

專案背景:電商平台重構
- 桌面版、行動版、小程式三個客戶端
- 商品詳情頁需要展示:基本資訊、規格、評論、推薦、優惠券等
- 不同平台展示的資料結構差異很大

最初方案 (RESTful):
GET /api/products/123
GET /api/products/123/specs
GET /api/products/123/reviews
GET /api/products/123/recommendations
GET /api/products/123/coupons

問題:
- 行動版只需要部分資料,卻要請求所有 API
- 桌面版需要更多細節,又要加新的 API
- 小程式有特殊需求,API 越來越多

改用 GraphQL 後:
// 行動版 - 只請求基本資訊
query MobileProduct {
  product(id: "123") {
    name
    price
    mainImage
    reviews(limit: 3) { rating }
  }
}

// 桌面版 - 請求完整資訊
query DesktopProduct {
  product(id: "123") {
    name
    price
    description
    images
    specs { ... }
    reviews(limit: 20) { ... }
    recommendations { ... }
  }
}

效果:
- API 請求數減少 60%
- 資料傳輸量減少 40%
- 開發新頁面時間減少 50%

💻 實戰演練:從零到一

實作範例 1:現代化的 RESTful API 客戶端

讓我們打造一個具備完整錯誤處理、快取機制和請求攔截的 API 客戶端:

/**
 * 現代化 RESTful API 客戶端
 * 特性:型別安全、自動重試、請求快取、錯誤處理
 */

interface ApiClientConfig {
  baseURL: string;
  timeout?: number;
  retryAttempts?: number;
  cacheTime?: number;
  headers?: Record<string, string>;
}

interface RequestConfig {
  method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
  url: string;
  data?: any;
  params?: Record<string, any>;
  cache?: boolean;
}

class ApiClient {
  private config: Required<ApiClientConfig>;
  private cache: Map<string, { data: any; timestamp: number }>;
  private requestInterceptors: Array<(config: RequestConfig) => RequestConfig> = [];
  private responseInterceptors: Array<(response: any) => any> = [];

  constructor(config: ApiClientConfig) {
    this.config = {
      timeout: 10000,
      retryAttempts: 3,
      cacheTime: 5 * 60 * 1000, // 5 分鐘
      headers: {},
      ...config,
    };
    this.cache = new Map();
  }

  /**
   * 添加請求攔截器
   * 用途:統一添加認證 token、追蹤請求等
   */
  addRequestInterceptor(interceptor: (config: RequestConfig) => RequestConfig) {
    this.requestInterceptors.push(interceptor);
  }

  /**
   * 添加回應攔截器
   * 用途:統一錯誤處理、資料轉換等
   */
  addResponseInterceptor(interceptor: (response: any) => any) {
    this.responseInterceptors.push(interceptor);
  }

  /**
   * 生成快取鍵值
   */
  private getCacheKey(config: RequestConfig): string {
    const { method, url, params } = config;
    return `${method}:${url}:${JSON.stringify(params || {})}`;
  }

  /**
   * 檢查快取
   */
  private getCache(key: string): any | null {
    const cached = this.cache.get(key);
    if (!cached) return null;

    const isExpired = Date.now() - cached.timestamp > this.config.cacheTime;
    if (isExpired) {
      this.cache.delete(key);
      return null;
    }

    return cached.data;
  }

  /**
   * 設定快取
   */
  private setCache(key: string, data: any): void {
    this.cache.set(key, {
      data,
      timestamp: Date.now(),
    });
  }

  /**
   * 執行請求(含重試機制)
   */
  private async executeRequest(
    config: RequestConfig,
    attempt: number = 1
  ): Promise<any> {
    try {
      // 套用請求攔截器
      let finalConfig = { ...config };
      for (const interceptor of this.requestInterceptors) {
        finalConfig = interceptor(finalConfig);
      }

      // 建立 AbortController 用於逾時控制
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);

      // 建立請求 URL
      const url = new URL(finalConfig.url, this.config.baseURL);
      if (finalConfig.params) {
        Object.entries(finalConfig.params).forEach(([key, value]) => {
          url.searchParams.append(key, String(value));
        });
      }

      // 發送請求
      const response = await fetch(url.toString(), {
        method: finalConfig.method,
        headers: {
          'Content-Type': 'application/json',
          ...this.config.headers,
        },
        body: finalConfig.data ? JSON.stringify(finalConfig.data) : undefined,
        signal: controller.signal,
      });

      clearTimeout(timeoutId);

      // 處理錯誤回應
      if (!response.ok) {
        throw new ApiError(
          `HTTP ${response.status}: ${response.statusText}`,
          response.status,
          await response.text()
        );
      }

      // 解析回應
      let data = await response.json();

      // 套用回應攔截器
      for (const interceptor of this.responseInterceptors) {
        data = interceptor(data);
      }

      return data;
    } catch (error) {
      // 重試邏輯
      if (attempt < this.config.retryAttempts && this.shouldRetry(error)) {
        console.warn(`Request failed, retrying (${attempt}/${this.config.retryAttempts})...`);
        await this.delay(Math.pow(2, attempt) * 1000); // 指數退避
        return this.executeRequest(config, attempt + 1);
      }

      throw error;
    }
  }

  /**
   * 判斷是否應該重試
   */
  private shouldRetry(error: any): boolean {
    // 網路錯誤或 5xx 錯誤才重試
    if (error.name === 'AbortError') return false;
    if (error instanceof ApiError) {
      return error.status >= 500;
    }
    return true;
  }

  /**
   * 延遲工具函式
   */
  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  /**
   * 發送請求(公開方法)
   */
  async request<T = any>(config: RequestConfig): Promise<T> {
    // GET 請求檢查快取
    if (config.method === 'GET' && config.cache !== false) {
      const cacheKey = this.getCacheKey(config);
      const cached = this.getCache(cacheKey);
      if (cached) {
        console.log(`Cache hit: ${cacheKey}`);
        return cached;
      }

      const data = await this.executeRequest(config);
      this.setCache(cacheKey, data);
      return data;
    }

    return this.executeRequest(config);
  }

  /**
   * GET 請求
   */
  get<T = any>(url: string, params?: Record<string, any>, cache = true): Promise<T> {
    return this.request<T>({ method: 'GET', url, params, cache });
  }

  /**
   * POST 請求
   */
  post<T = any>(url: string, data?: any): Promise<T> {
    return this.request<T>({ method: 'POST', url, data });
  }

  /**
   * PUT 請求
   */
  put<T = any>(url: string, data?: any): Promise<T> {
    return this.request<T>({ method: 'PUT', url, data });
  }

  /**
   * DELETE 請求
   */
  delete<T = any>(url: string): Promise<T> {
    return this.request<T>({ method: 'DELETE', url });
  }

  /**
   * 清除快取
   */
  clearCache(pattern?: string): void {
    if (!pattern) {
      this.cache.clear();
      return;
    }

    const regex = new RegExp(pattern);
    for (const [key] of this.cache) {
      if (regex.test(key)) {
        this.cache.delete(key);
      }
    }
  }
}

/**
 * 自訂錯誤類別
 */
class ApiError extends Error {
  constructor(
    message: string,
    public status: number,
    public responseText: string
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

// 使用範例
const api = new ApiClient({
  baseURL: 'https://api.example.com',
  timeout: 15000,
  retryAttempts: 3,
  cacheTime: 10 * 60 * 1000, // 10 分鐘
});

// 添加認證攔截器
api.addRequestInterceptor((config) => {
  const token = localStorage.getItem('auth_token');
  if (token) {
    config.headers = {
      ...config.headers,
      Authorization: `Bearer ${token}`,
    };
  }
  return config;
});

// 添加錯誤處理攔截器
api.addResponseInterceptor((response) => {
  if (response.code !== 0) {
    throw new ApiError(
      response.message || 'Unknown error',
      response.code,
      JSON.stringify(response)
    );
  }
  return response.data;
});

// 實際使用
async function fetchUserProfile(userId: string) {
  try {
    const user = await api.get(`/users/${userId}`);
    console.log('User profile:', user);
    return user;
  } catch (error) {
    if (error instanceof ApiError) {
      console.error(`API Error ${error.status}:`, error.message);
      // 根據錯誤碼做不同處理
      if (error.status === 401) {
        // 重新登入
      } else if (error.status === 404) {
        // 顯示使用者不存在
      }
    } else {
      console.error('Network error:', error);
    }
  }
}

實作範例 2:GraphQL 客戶端實作

現在讓我們實作一個輕量級的 GraphQL 客戶端:

/**
 * 輕量級 GraphQL 客戶端
 * 特性:型別安全、自動持久化查詢、批次請求
 */

interface GraphQLClientConfig {
  endpoint: string;
  headers?: Record<string, string>;
  persistedQueries?: boolean;
  batchInterval?: number;
}

interface GraphQLRequest {
  query: string;
  variables?: Record<string, any>;
  operationName?: string;
}

interface GraphQLResponse<T = any> {
  data?: T;
  errors?: Array<{
    message: string;
    locations?: Array<{ line: number; column: number }>;
    path?: string[];
  }>;
}

class GraphQLClient {
  private config: Required<GraphQLClientConfig>;
  private queryHashCache: Map<string, string>;
  private pendingRequests: Array<{
    request: GraphQLRequest;
    resolve: (value: any) => void;
    reject: (error: any) => void;
  }>;
  private batchTimeout: NodeJS.Timeout | null;

  constructor(config: GraphQLClientConfig) {
    this.config = {
      headers: {},
      persistedQueries: false,
      batchInterval: 10,
      ...config,
    };
    this.queryHashCache = new Map();
    this.pendingRequests = [];
    this.batchTimeout = null;
  }

  /**
   * 計算查詢的 SHA256 雜湊(用於持久化查詢)
   */
  private async hashQuery(query: string): Promise<string> {
    if (this.queryHashCache.has(query)) {
      return this.queryHashCache.get(query)!;
    }

    const encoder = new TextEncoder();
    const data = encoder.encode(query);
    const hashBuffer = await crypto.subtle.digest('SHA-256', data);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');

    this.queryHashCache.set(query, hashHex);
    return hashHex;
  }

  /**
   * 發送單一請求
   */
  private async sendRequest<T = any>(
    request: GraphQLRequest
  ): Promise<GraphQLResponse<T>> {
    const body: any = {
      query: request.query,
      variables: request.variables,
      operationName: request.operationName,
    };

    // 持久化查詢
    if (this.config.persistedQueries) {
      const queryHash = await this.hashQuery(request.query);
      body.extensions = {
        persistedQuery: {
          version: 1,
          sha256Hash: queryHash,
        },
      };
      delete body.query; // 第一次嘗試不發送完整查詢
    }

    try {
      const response = await fetch(this.config.endpoint, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...this.config.headers,
        },
        body: JSON.stringify(body),
      });

      const result: GraphQLResponse<T> = await response.json();

      // 如果伺服器不支援持久化查詢,重新發送完整查詢
      if (
        this.config.persistedQueries &&
        result.errors?.some(e => e.message.includes('PersistedQueryNotFound'))
      ) {
        body.query = request.query;
        const retryResponse = await fetch(this.config.endpoint, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            ...this.config.headers,
          },
          body: JSON.stringify(body),
        });
        return retryResponse.json();
      }

      return result;
    } catch (error) {
      throw new GraphQLError('Network error', error);
    }
  }

  /**
   * 發送批次請求
   */
  private async sendBatchRequests(): Promise<void> {
    if (this.pendingRequests.length === 0) return;

    const batch = this.pendingRequests.splice(0);
    const requests = batch.map(({ request }) => request);

    try {
      const response = await fetch(this.config.endpoint, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...this.config.headers,
        },
        body: JSON.stringify(requests),
      });

      const results: GraphQLResponse[] = await response.json();

      // 分發結果
      results.forEach((result, index) => {
        if (result.errors && result.errors.length > 0) {
          batch[index].reject(new GraphQLError('GraphQL errors', result.errors));
        } else {
          batch[index].resolve(result.data);
        }
      });
    } catch (error) {
      // 批次失敗,所有請求都拋出錯誤
      batch.forEach(({ reject }) => {
        reject(new GraphQLError('Batch request failed', error));
      });
    }
  }

  /**
   * 排程批次請求
   */
  private scheduleBatchRequest(): void {
    if (this.batchTimeout) return;

    this.batchTimeout = setTimeout(() => {
      this.batchTimeout = null;
      this.sendBatchRequests();
    }, this.config.batchInterval);
  }

  /**
   * 執行 GraphQL 請求
   */
  async request<T = any>(
    query: string,
    variables?: Record<string, any>,
    options: { batch?: boolean; operationName?: string } = {}
  ): Promise<T> {
    const request: GraphQLRequest = {
      query: query.trim(),
      variables,
      operationName: options.operationName,
    };

    // 批次請求
    if (options.batch) {
      return new Promise((resolve, reject) => {
        this.pendingRequests.push({ request, resolve, reject });
        this.scheduleBatchRequest();
      });
    }

    // 單一請求
    const response = await this.sendRequest<T>(request);

    if (response.errors && response.errors.length > 0) {
      throw new GraphQLError('GraphQL errors', response.errors);
    }

    return response.data as T;
  }

  /**
   * 執行查詢
   */
  query<T = any>(
    query: string,
    variables?: Record<string, any>,
    options?: { batch?: boolean }
  ): Promise<T> {
    return this.request<T>(query, variables, options);
  }

  /**
   * 執行變更
   */
  mutate<T = any>(
    mutation: string,
    variables?: Record<string, any>
  ): Promise<T> {
    return this.request<T>(mutation, variables, { batch: false });
  }

  /**
   * 訂閱(需要 WebSocket 支援)
   */
  subscribe(
    subscription: string,
    variables?: Record<string, any>,
    callback?: (data: any) => void
  ): () => void {
    // WebSocket 實作(簡化版)
    const wsEndpoint = this.config.endpoint.replace(/^http/, 'ws');
    const ws = new WebSocket(wsEndpoint, 'graphql-ws');

    ws.onopen = () => {
      ws.send(JSON.stringify({
        type: 'connection_init',
      }));

      ws.send(JSON.stringify({
        type: 'start',
        payload: {
          query: subscription,
          variables,
        },
      }));
    };

    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      if (message.type === 'data' && callback) {
        callback(message.payload.data);
      }
    };

    // 返回取消訂閱函式
    return () => {
      ws.send(JSON.stringify({ type: 'stop' }));
      ws.close();
    };
  }
}

/**
 * GraphQL 錯誤類別
 */
class GraphQLError extends Error {
  constructor(message: string, public details: any) {
    super(message);
    this.name = 'GraphQLError';
  }
}

// 使用範例
const graphql = new GraphQLClient({
  endpoint: 'https://api.example.com/graphql',
  persistedQueries: true,
  batchInterval: 10,
});

// 型別定義
interface User {
  id: string;
  name: string;
  email: string;
  posts: Array<{
    id: string;
    title: string;
    createdAt: string;
  }>;
}

// 查詢範例
async function fetchUser(userId: string) {
  const query = `
    query GetUser($userId: ID!) {
      user(id: $userId) {
        id
        name
        email
        posts(limit: 10) {
          id
          title
          createdAt
        }
      }
    }
  `;

  try {
    const data = await graphql.query<{ user: User }>(
      query,
      { userId },
      { batch: true }
    );

    console.log('User data:', data.user);
    return data.user;
  } catch (error) {
    if (error instanceof GraphQLError) {
      console.error('GraphQL Error:', error.details);
    } else {
      console.error('Network Error:', error);
    }
  }
}

// 變更範例
async function updateUserProfile(userId: string, name: string) {
  const mutation = `
    mutation UpdateUser($userId: ID!, $name: String!) {
      updateUser(id: $userId, input: { name: $name }) {
        id
        name
        updatedAt
      }
    }
  `;

  const data = await graphql.mutate(mutation, { userId, name });
  return data;
}

// 訂閱範例
function subscribeToNewPosts(userId: string) {
  const subscription = `
    subscription OnNewPost($userId: ID!) {
      postCreated(userId: $userId) {
        id
        title
        createdAt
      }
    }
  `;

  const unsubscribe = graphql.subscribe(
    subscription,
    { userId },
    (data) => {
      console.log('New post:', data.postCreated);
      // 更新 UI
    }
  );

  // 需要時取消訂閱
  return unsubscribe;
}

實作範例 3:智能快取策略實作

讓我們實作一個進階的快取管理系統:

/**
 * 進階快取管理系統
 * 特性:多層快取、自動失效、依賴追蹤
 */

interface CacheConfig {
  ttl?: number; // 生存時間 (毫秒)
  staleWhileRevalidate?: number; // 過期後仍可使用的時間
  maxSize?: number; // 最大快取數量
  storage?: 'memory' | 'localStorage' | 'indexedDB';
}

interface CacheEntry<T> {
  data: T;
  timestamp: number;
  ttl: number;
  dependencies?: Set<string>;
  tags?: Set<string>;
}

class CacheManager {
  private memoryCache: Map<string, CacheEntry<any>>;
  private defaultConfig: Required<CacheConfig>;
  private accessCount: Map<string, number>;

  constructor(config: CacheConfig = {}) {
    this.memoryCache = new Map();
    this.accessCount = new Map();
    this.defaultConfig = {
      ttl: 5 * 60 * 1000, // 5 分鐘
      staleWhileRevalidate: 60 * 1000, // 1 分鐘
      maxSize: 100,
      storage: 'memory',
      ...config,
    };

    // 定期清理過期快取
    setInterval(() => this.cleanup(), 60 * 1000);
  }

  /**
   * 生成快取鍵值
   */
  private generateKey(key: string | any[]): string {
    if (typeof key === 'string') return key;
    return JSON.stringify(key);
  }

  /**
   * 檢查快取是否過期
   */
  private isExpired(entry: CacheEntry<any>): boolean {
    return Date.now() - entry.timestamp > entry.ttl;
  }

  /**
   * 檢查快取是否在 stale-while-revalidate 期間內
   */
  private isStale(entry: CacheEntry<any>): boolean {
    const age = Date.now() - entry.timestamp;
    return age > entry.ttl && age <= entry.ttl + this.defaultConfig.staleWhileRevalidate;
  }

  /**
   * LRU 淘汰策略
   */
  private evictLRU(): void {
    if (this.memoryCache.size < this.defaultConfig.maxSize) return;

    // 找出存取次數最少的項目
    let minAccess = Infinity;
    let lruKey: string | null = null;

    for (const [key] of this.memoryCache) {
      const access = this.accessCount.get(key) || 0;
      if (access < minAccess) {
        minAccess = access;
        lruKey = key;
      }
    }

    if (lruKey) {
      this.memoryCache.delete(lruKey);
      this.accessCount.delete(lruKey);
    }
  }

  /**
   * 設定快取
   */
  set<T>(
    key: string | any[],
    data: T,
    options: {
      ttl?: number;
      dependencies?: string[];
      tags?: string[];
    } = {}
  ): void {
    const cacheKey = this.generateKey(key);

    // 檢查容量並淘汰
    this.evictLRU();

    const entry: CacheEntry<T> = {
      data,
      timestamp: Date.now(),
      ttl: options.ttl || this.defaultConfig.ttl,
      dependencies: options.dependencies ? new Set(options.dependencies) : undefined,
      tags: options.tags ? new Set(options.tags) : undefined,
    };

    this.memoryCache.set(cacheKey, entry);
    this.accessCount.set(cacheKey, 0);
  }

  /**
   * 取得快取
   */
  get<T>(key: string | any[]): T | null {
    const cacheKey = this.generateKey(key);
    const entry = this.memoryCache.get(cacheKey);

    if (!entry) return null;

    // 更新存取計數
    this.accessCount.set(cacheKey, (this.accessCount.get(cacheKey) || 0) + 1);

    // 完全過期,返回 null
    if (this.isExpired(entry) && !this.isStale(entry)) {
      this.memoryCache.delete(cacheKey);
      return null;
    }

    return entry.data as T;
  }

  /**
   * 取得快取(含 stale-while-revalidate)
   */
  async getOrFetch<T>(
    key: string | any[],
    fetcher: () => Promise<T>,
    options?: {
      ttl?: number;
      dependencies?: string[];
      tags?: string[];
    }
  ): Promise<T> {
    const cached = this.get<T>(key);
    const cacheKey = this.generateKey(key);
    const entry = this.memoryCache.get(cacheKey);

    // 快取存在且未過期,直接返回
    if (cached && entry && !this.isExpired(entry)) {
      return cached;
    }

    // 快取過期但在 stale-while-revalidate 期間內
    // 返回舊資料,背景更新
    if (cached && entry && this.isStale(entry)) {
      // 背景重新取得資料
      fetcher()
        .then(data => {
          this.set(key, data, options);
        })
        .catch(err => {
          console.error('Background revalidation failed:', err);
        });

      return cached;
    }

    // 快取不存在或完全過期,重新取得
    const data = await fetcher();
    this.set(key, data, options);
    return data;
  }

  /**
   * 使快取失效(根據鍵值)
   */
  invalidate(key: string | any[]): void {
    const cacheKey = this.generateKey(key);
    this.memoryCache.delete(cacheKey);
    this.accessCount.delete(cacheKey);
  }

  /**
   * 使快取失效(根據標籤)
   */
  invalidateByTag(tag: string): void {
    for (const [key, entry] of this.memoryCache) {
      if (entry.tags?.has(tag)) {
        this.memoryCache.delete(key);
        this.accessCount.delete(key);
      }
    }
  }

  /**
   * 使快取失效(根據依賴)
   */
  invalidateByDependency(dependency: string): void {
    for (const [key, entry] of this.memoryCache) {
      if (entry.dependencies?.has(dependency)) {
        this.memoryCache.delete(key);
        this.accessCount.delete(key);
      }
    }
  }

  /**
   * 清理過期快取
   */
  private cleanup(): void {
    const now = Date.now();
    for (const [key, entry] of this.memoryCache) {
      const age = now - entry.timestamp;
      const maxAge = entry.ttl + this.defaultConfig.staleWhileRevalidate;

      if (age > maxAge) {
        this.memoryCache.delete(key);
        this.accessCount.delete(key);
      }
    }
  }

  /**
   * 清空所有快取
   */
  clear(): void {
    this.memoryCache.clear();
    this.accessCount.clear();
  }

  /**
   * 取得快取統計資訊
   */
  getStats() {
    return {
      size: this.memoryCache.size,
      maxSize: this.defaultConfig.maxSize,
      hitRate: this.calculateHitRate(),
      entries: Array.from(this.memoryCache.entries()).map(([key, entry]) => ({
        key,
        age: Date.now() - entry.timestamp,
        accessCount: this.accessCount.get(key) || 0,
      })),
    };
  }

  /**
   * 計算快取命中率
   */
  private calculateHitRate(): number {
    const totalAccess = Array.from(this.accessCount.values()).reduce((a, b) => a + b, 0);
    const uniqueKeys = this.accessCount.size;
    return uniqueKeys === 0 ? 0 : totalAccess / uniqueKeys;
  }
}

// 整合到 API 客戶端
class CachedApiClient extends ApiClient {
  private cacheManager: CacheManager;

  constructor(config: ApiClientConfig, cacheConfig?: CacheConfig) {
    super(config);
    this.cacheManager = new CacheManager(cacheConfig);
  }

  /**
   * 帶快取的 GET 請求
   */
  async get<T = any>(
    url: string,
    params?: Record<string, any>,
    options: {
      cache?: boolean;
      ttl?: number;
      tags?: string[];
      dependencies?: string[];
    } = {}
  ): Promise<T> {
    const cacheKey = ['GET', url, params];

    if (options.cache === false) {
      return super.get<T>(url, params, false);
    }

    return this.cacheManager.getOrFetch(
      cacheKey,
      () => super.get<T>(url, params, false),
      {
        ttl: options.ttl,
        tags: options.tags,
        dependencies: options.dependencies,
      }
    );
  }

  /**
   * 變更操作後使快取失效
   */
  async post<T = any>(
    url: string,
    data?: any,
    options: {
      invalidateTags?: string[];
      invalidateDependencies?: string[];
    } = {}
  ): Promise<T> {
    const result = await super.post<T>(url, data);

    // 使相關快取失效
    if (options.invalidateTags) {
      options.invalidateTags.forEach(tag => {
        this.cacheManager.invalidateByTag(tag);
      });
    }

    if (options.invalidateDependencies) {
      options.invalidateDependencies.forEach(dep => {
        this.cacheManager.invalidateByDependency(dep);
      });
    }

    return result;
  }

  /**
   * 取得快取統計
   */
  getCacheStats() {
    return this.cacheManager.getStats();
  }
}

// 使用範例
const api = new CachedApiClient(
  {
    baseURL: 'https://api.example.com',
  },
  {
    ttl: 5 * 60 * 1000,
    staleWhileRevalidate: 60 * 1000,
    maxSize: 200,
  }
);

// 使用快取和標籤
async function fetchProducts() {
  return api.get('/products', undefined, {
    cache: true,
    ttl: 10 * 60 * 1000,
    tags: ['products'],
    dependencies: ['catalog'],
  });
}

// 新增產品後使相關快取失效
async function createProduct(productData: any) {
  return api.post('/products', productData, {
    invalidateTags: ['products'],
    invalidateDependencies: ['catalog'],
  });
}

// 檢視快取效能
console.log('Cache stats:', api.getCacheStats());

📋 本日重點回顧

  1. 核心概念: API 設計直接影響前端開發效率和使用者體驗。RESTful 和 GraphQL 各有優勢,需要根據專案特性選擇合適方案。

  2. 關鍵技術:

    • 完整的錯誤處理和重試機制是生產級 API 客戶端的必備特性
    • GraphQL 的精確資料獲取能顯著減少網路請求次數
    • 多層快取策略(memory + stale-while-revalidate)能大幅提升效能
  3. 實踐要點:

    • 型別安全是現代 API 客戶端的基本要求
    • 合理的快取策略能減少 60% 以上的 API 請求
    • 依賴追蹤和標籤系統讓快取失效管理變得簡單

🎯 最佳實踐建議

  • 推薦做法: 使用 TypeScript 定義 API 回應型別,確保型別安全

  • 推薦做法: 實作完整的錯誤處理機制,包括重試、逾時和錯誤分類

  • 推薦做法: 使用 stale-while-revalidate 策略平衡即時性和效能

  • 推薦做法: 為不同類型的資料設定合理的快取時間(使用者資料 5分鐘,靜態資料 1小時)

  • 避免陷阱: 不要過度快取動態資料,避免使用者看到過期內容

  • 避免陷阱: 不要忽略快取容量控制,避免記憶體洩漏

  • 避免陷阱: 變更操作後不要忘記使相關快取失效

  • 避免陷阱: 不要在錯誤處理中暴露敏感資訊給使用者

🤔 延伸思考

  1. 架構選擇: 你的專案更適合 RESTful 還是 GraphQL?考慮團隊技術棧、資料關聯複雜度和客戶端多樣性。

  2. 效能最佳化: 如何設計一個既能減少請求次數,又能保持資料即時性的快取策略?

  3. 實踐挑戰: 嘗試在現有專案中實作請求批次處理和智能快取,測量實際效能提升。


上一篇
狀態管理選擇困難症:從 Redux 到 Zustand 的現代化方案
下一篇
響應式設計 2.0:Container Queries 與現代化布局技術
系列文
前端工程師的 Modern Web 實踐之道13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言