Axios 深度剖析:攔截器原理與對比 Fetch
Axios 是目前最流行的 HTTP 客戶端函式庫之一。它封裝了 XMLHttpRequest,提供了比原生 API 更強大、更易用的功能。
一、 為什麼選擇 Axios?
1.1 Axios vs Fetch 快速對比
| 特性 | Axios | Fetch |
|---|---|---|
| 請求/回應攔截器 | ✅ 內建 | ❌ 需自己實作 |
| 自動 JSON 轉換 | ✅ 自動 | ❌ 需手動 |
| 錯誤處理 | ✅ 自動 reject 4xx/5xx | ❌ 不 reject |
| 取消請求 | ✅ 內建 | ⚠️ 需 AbortController |
| 上傳進度 | ✅ 內建 | ❌ 不支援 |
| Node.js | ✅ 支援 | ⚠️ 需 node-fetch |
| 套件大小 | ~13KB | 0(原生) |
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
- 兩者都行時 → 看團隊習慣
進階挑戰
- 自己用 Fetch 實作一個類似 Axios 的攔截器機制。
- 封裝一個支援請求緩存的 Axios 實例。
- 實作一個請求合併/去重機制,避免短時間內重複請求同一 API。