跳至主要內容
Skip to content

Axios 深度剖析:攔截器原理與對比 Fetch

Axios 是目前最流行的 HTTP 客戶端函式庫之一。它封裝了 XMLHttpRequest,提供了比原生 API 更強大、更易用的功能。


一、 為什麼選擇 Axios?

1.1 Axios vs Fetch 快速對比

特性AxiosFetch
請求/回應攔截器✅ 內建❌ 需自己實作
自動 JSON 轉換✅ 自動❌ 需手動
錯誤處理✅ 自動 reject 4xx/5xx❌ 不 reject
取消請求✅ 內建⚠️ 需 AbortController
上傳進度✅ 內建❌ 不支援
Node.js✅ 支援⚠️ 需 node-fetch
套件大小~13KB0(原生)

1.2 安裝

bash
npm install axios

二、 基本用法

2.1 發送請求

javascript
import axios from "axios";

// GET
const response = await axios.get("/api/users");
console.log(response.data);

// POST
const result = await axios.post("/api/users", {
  name: "John",
  email: "john@example.com",
});

// 完整配置
const response = await axios({
  method: "post",
  url: "/api/users",
  data: { name: "John" },
  headers: { Authorization: "Bearer token" },
});

2.2 Response 結構

javascript
const response = await axios.get("/api/users");

console.log(response.data); // 回應資料(已解析)
console.log(response.status); // 200
console.log(response.statusText); // "OK"
console.log(response.headers); // 回應標頭
console.log(response.config); // 請求配置
console.log(response.request); // XMLHttpRequest 實例

2.3 建立實例

javascript
const api = axios.create({
  baseURL: "https://api.example.com",
  timeout: 10000,
  headers: {
    "Content-Type": "application/json",
  },
});

// 使用實例
const users = await api.get("/users");
const user = await api.post("/users", { name: "John" });

三、 請求配置

3.1 完整配置選項

javascript
axios({
  // URL 和方法
  url: "/users",
  method: "get",
  baseURL: "https://api.example.com",

  // Headers
  headers: {
    Authorization: "Bearer token",
    "X-Custom-Header": "value",
  },

  // URL 參數
  params: { page: 1, limit: 10 },
  paramsSerializer: (params) => qs.stringify(params),

  // 請求 Body
  data: { name: "John" },

  // 超時(毫秒)
  timeout: 5000,

  // 認證
  auth: { username: "user", password: "pass" },

  // 回應類型
  responseType: "json", // 'arraybuffer', 'blob', 'document', 'text'

  // 跨域
  withCredentials: true,

  // 進度
  onUploadProgress: (progressEvent) => {},
  onDownloadProgress: (progressEvent) => {},

  // 驗證狀態碼
  validateStatus: (status) => status < 500,

  // 取消
  signal: controller.signal,

  // 其他
  maxRedirects: 5,
  maxContentLength: 2000,
  decompress: true,
});

四、 攔截器(Interceptors)

攔截器是 Axios 最強大的特性之一。

4.1 請求攔截器

javascript
axios.interceptors.request.use(
  (config) => {
    // 在發送請求前做些什麼
    const token = localStorage.getItem("token");
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    console.log("請求發送:", config.url);
    return config;
  },
  (error) => {
    // 請求錯誤時做些什麼
    return Promise.reject(error);
  }
);

4.2 回應攔截器

javascript
axios.interceptors.response.use(
  (response) => {
    // 2xx 範圍的狀態碼觸發
    console.log("收到回應:", response.status);
    return response;
  },
  (error) => {
    // 非 2xx 範圍的狀態碼觸發
    if (error.response?.status === 401) {
      // Token 過期,重新登入
      window.location.href = "/login";
    }
    return Promise.reject(error);
  }
);

4.3 移除攔截器

javascript
const interceptorId = axios.interceptors.request.use(...)

// 移除
axios.interceptors.request.eject(interceptorId)

4.4 實例攔截器

javascript
const api = axios.create({ baseURL: "/api" });

// 只影響這個實例
api.interceptors.request.use((config) => {
  config.headers["X-Instance"] = "api";
  return config;
});

五、 攔截器實戰

5.1 自動刷新 Token

javascript
let isRefreshing = false;
let failedQueue = [];

const processQueue = (error, token = null) => {
  failedQueue.forEach(({ resolve, reject }) => {
    if (error) {
      reject(error);
    } else {
      resolve(token);
    }
  });
  failedQueue = [];
};

api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;

    if (error.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        // 等待刷新完成
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject });
        }).then((token) => {
          originalRequest.headers.Authorization = `Bearer ${token}`;
          return api(originalRequest);
        });
      }

      originalRequest._retry = true;
      isRefreshing = true;

      try {
        const { data } = await axios.post("/auth/refresh", {
          refreshToken: localStorage.getItem("refreshToken"),
        });

        localStorage.setItem("token", data.accessToken);
        processQueue(null, data.accessToken);

        originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
        return api(originalRequest);
      } catch (refreshError) {
        processQueue(refreshError);
        localStorage.clear();
        window.location.href = "/login";
        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false;
      }
    }

    return Promise.reject(error);
  }
);

5.2 請求重試

javascript
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const config = error.config;

    if (!config || !config.retry) {
      return Promise.reject(error);
    }

    config._retryCount = config._retryCount || 0;

    if (config._retryCount >= config.retry) {
      return Promise.reject(error);
    }

    config._retryCount++;

    // 指數退避
    const delay = Math.pow(2, config._retryCount) * 1000;
    await new Promise((resolve) => setTimeout(resolve, delay));

    return api(config);
  }
);

// 使用
api.get("/api/data", { retry: 3 });

5.3 請求日誌

javascript
api.interceptors.request.use((config) => {
  config._startTime = Date.now();
  console.log(`${config.method?.toUpperCase()} ${config.url}`);
  return config;
});

api.interceptors.response.use(
  (response) => {
    const duration = Date.now() - response.config._startTime;
    console.log(`${response.status} ${response.config.url} (${duration}ms)`);
    return response;
  },
  (error) => {
    const duration = Date.now() - error.config?._startTime;
    console.error(
      `${error.response?.status || "ERR"} ${
        error.config?.url
      } (${duration}ms)`
    );
    return Promise.reject(error);
  }
);

六、 取消請求

6.1 使用 AbortController

javascript
const controller = new AbortController();

axios
  .get("/api/data", {
    signal: controller.signal,
  })
  .catch((error) => {
    if (axios.isCancel(error)) {
      console.log("請求已取消");
    }
  });

// 取消
controller.abort();

6.2 CancelToken(舊版 API)

javascript
const source = axios.CancelToken.source();

axios
  .get("/api/data", {
    cancelToken: source.token,
  })
  .catch((error) => {
    if (axios.isCancel(error)) {
      console.log("取消:", error.message);
    }
  });

// 取消
source.cancel("User cancelled");

七、 進度追蹤

7.1 下載進度

javascript
axios.get("/files/large.zip", {
  responseType: "blob",
  onDownloadProgress: (progressEvent) => {
    const percent = Math.round(
      (progressEvent.loaded * 100) / progressEvent.total
    );
    console.log(`下載進度: ${percent}%`);
  },
});

7.2 上傳進度

javascript
const formData = new FormData();
formData.append("file", file);

axios.post("/api/upload", formData, {
  headers: { "Content-Type": "multipart/form-data" },
  onUploadProgress: (progressEvent) => {
    const percent = Math.round(
      (progressEvent.loaded * 100) / progressEvent.total
    );
    console.log(`上傳進度: ${percent}%`);
  },
});

八、 錯誤處理

8.1 錯誤結構

javascript
try {
  await axios.get("/api/data");
} catch (error) {
  if (error.response) {
    // 伺服器回應了,但狀態碼不在 2xx 範圍
    console.log("資料:", error.response.data);
    console.log("狀態:", error.response.status);
    console.log("標頭:", error.response.headers);
  } else if (error.request) {
    // 請求已發出,但沒有收到回應
    console.log("無回應:", error.request);
  } else {
    // 設定請求時發生錯誤
    console.log("錯誤:", error.message);
  }
}

8.2 自訂錯誤處理

javascript
axios.interceptors.response.use(
  (response) => response,
  (error) => {
    const customError = {
      message: error.response?.data?.message || error.message,
      status: error.response?.status,
      code: error.response?.data?.code,
      original: error,
    };

    return Promise.reject(customError);
  }
);

九、 Axios vs Fetch 詳細對比

9.1 相同功能對比

javascript
// === GET 請求 ===

// Fetch
const response = await fetch("/api/users");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();

// Axios
const { data } = await axios.get("/api/users");

// === POST JSON ===

// Fetch
await fetch("/api/users", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "John" }),
});

// Axios
await axios.post("/api/users", { name: "John" });

// === 超時 ===

// Fetch
await fetch("/api/data", {
  signal: AbortSignal.timeout(5000),
});

// Axios
await axios.get("/api/data", { timeout: 5000 });

9.2 何時選擇 Axios

  • 需要攔截器
  • 需要上傳進度
  • 需要自動 JSON 處理
  • 需要統一錯誤處理
  • 需要支援 Node.js
  • 專案已在使用

9.3 何時選擇 Fetch

  • 不想增加依賴
  • 簡單請求
  • 只需要現代瀏覽器
  • 需要串流處理
  • 套件大小敏感

十、 封裝最佳實踐

javascript
import axios from "axios";

class ApiClient {
  constructor(baseURL) {
    this.client = axios.create({
      baseURL,
      timeout: 10000,
      headers: {
        "Content-Type": "application/json",
      },
    });

    this.setupInterceptors();
  }

  setupInterceptors() {
    // 請求攔截
    this.client.interceptors.request.use((config) => {
      const token = this.getToken();
      if (token) {
        config.headers.Authorization = `Bearer ${token}`;
      }
      return config;
    });

    // 回應攔截
    this.client.interceptors.response.use(
      (response) => response.data,
      this.handleError.bind(this)
    );
  }

  getToken() {
    return localStorage.getItem("token");
  }

  handleError(error) {
    if (error.response?.status === 401) {
      this.onUnauthorized();
    }
    return Promise.reject(error);
  }

  onUnauthorized() {
    window.location.href = "/login";
  }

  get(url, config) {
    return this.client.get(url, config);
  }

  post(url, data, config) {
    return this.client.post(url, data, config);
  }

  put(url, data, config) {
    return this.client.put(url, data, config);
  }

  delete(url, config) {
    return this.client.delete(url, config);
  }
}

export const api = new ApiClient("https://api.example.com");

總結

特性說明
實例建立axios.create() 獨立配置
攔截器請求/回應統一處理
自動轉換JSON 自動序列化/解析
錯誤處理4xx/5xx 自動 reject
進度追蹤上傳/下載進度回調

> **選擇建議**:

  • 大型專案、需要攔截器 → Axios
  • 簡單請求、減少依賴 → Fetch
  • 兩者都行時 → 看團隊習慣

進階挑戰

  1. 自己用 Fetch 實作一個類似 Axios 的攔截器機制。
  2. 封裝一個支援請求緩存的 Axios 實例。
  3. 實作一個請求合併/去重機制,避免短時間內重複請求同一 API。

延伸閱讀與資源