

API 互動方式演進
XMLHttpRequest (1999) → jQuery.ajax (2006) → Fetch API (2015) → Axios
│ │ │ │
│ │ │ └─ 功能完整
│ │ └─ 原生 Promise
│ └─ 簡化語法
└─ 底層 API
XMLHttpRequest (XHR)
最早期的非同步請求方式,語法較為繁瑣。
function fetchData(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(xhr.statusText));
}
};
xhr.onerror = function() {
reject(new Error('Network Error'));
};
xhr.send();
});
}
// 使用
fetchData('/api/users')
.then(data => console.log(data))
.catch(err => console.error(err));XHR 特點
- 瀏覽器原生支援
- 可以追蹤上傳/下載進度
- 語法繁瑣
Fetch API
現代瀏覽器原生支援,基於 Promise 的簡潔 API。
// GET 請求
const response = await fetch('/api/users');
const users = await response.json();
// POST 請求
const newUser = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'John',
email: 'john@example.com'
})
});
// 完整範例(含錯誤處理)
async function fetchUsers() {
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
}Fetch 注意事項
// ⚠️ fetch 只在網路錯誤時 reject
// HTTP 4xx/5xx 不會 reject,需要手動檢查
fetch('/api/not-found') // 404
.then(res => {
console.log(res.ok); // false
console.log(res.status); // 404
// 不會進入 catch!
});
// ⚠️ fetch 預設不帶 Cookie
fetch('/api/profile', {
credentials: 'include' // 跨域帶 Cookie
// 或 'same-origin' // 同源帶 Cookie
});
// 取消請求(例如使用者離開頁面)
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal });
controller.abort(); // 取消補充:
AbortController在實務上非常重要——當使用者快速切換頁面或元件 unmount 時,如果沒有取消進行中的請求,可能會導致 state 更新到已卸載的元件上。在 React 的useEffectcleanup 裡搭配AbortController是常見的做法。
Axios
功能更完整的 HTTP 客戶端。
import axios from 'axios';
// 建立實例
const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 5000,
headers: {
'Content-Type': 'application/json'
}
});
// GET 請求
const { data } = await api.get('/users');
// POST 請求
const response = await api.post('/users', {
name: 'John',
email: 'john@example.com'
});
// 攔截器
api.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
api.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
// 處理登出
window.location.href = '/login';
}
return Promise.reject(error);
}
);Axios vs Fetch
| 特性 | Fetch | Axios |
|---|---|---|
| 瀏覽器原生 | ✅ | ❌(需安裝) |
| 自動 JSON 轉換 | ❌ | ✅ |
| 請求攔截 | ❌ | ✅ |
| 請求取消 | AbortController | CancelToken |
| 錯誤處理 | 需手動檢查 | 自動 reject |
| 進度追蹤 | ❌ | ✅ |
Promise
處理非同步操作的標準方式。
// 建立 Promise
function delay(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
// Promise 鏈
fetch('/api/user/1')
.then(res => res.json())
.then(user => fetch(`/api/posts?userId=${user.id}`))
.then(res => res.json())
.then(posts => console.log(posts))
.catch(err => console.error(err));
// Promise 並行
const [users, posts] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json())
]);
// Promise 競速
const fastest = await Promise.race([
fetch('/api/server1'),
fetch('/api/server2')
]);async/await
Promise 的語法糖,讓非同步程式碼看起來像同步。
// 基本用法
async function getUser(id) {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
return user;
}
// 錯誤處理
async function getData() {
try {
const user = await getUser(1);
const posts = await getPosts(user.id);
return { user, posts };
} catch (error) {
console.error('Error:', error);
throw error;
}
}
// 並行執行
async function loadDashboard() {
const [user, notifications, stats] = await Promise.all([
getUser(),
getNotifications(),
getStats()
]);
return { user, notifications, stats };
}現代框架的資料請求
上面介紹的 fetch、axios 都是「發請求」的工具,但在真實的 React/Vue 專案裡,你很快會發現光是發請求不夠——你還需要管理 loading 狀態、error 狀態、快取、重新請求、分頁……全部自己手動管理會非常痛苦。這就是 React Query 和 SWR 出現的原因。
React Query (TanStack Query)
React Query 把「跟 server 拿資料」這件事抽象成一個 hook,幫你處理所有瑣碎的狀態管理。
import { useQuery, useMutation } from '@tanstack/react-query';
// 查詢資料
function UserList() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json()),
staleTime: 5 * 60 * 1000, // 5 分鐘內不重新請求
});
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return data.map(user => <div key={user.id}>{user.name}</div>);
}
// 修改資料
function CreateUser() {
const mutation = useMutation({
mutationFn: (newUser) => api.post('/users', newUser),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] }); // 自動重新請求
},
});
return <button onClick={() => mutation.mutate({ name: 'John' })}>新增</button>;
}用 React Query 你不用自己寫 useState 管 loading、error,也不用自己處理快取失效——它全部幫你搞定。
SWR
SWR 是 Vercel 出的替代方案,名字來自 HTTP 的 stale-while-revalidate 快取策略:先回傳快取的舊資料(stale),同時在背景重新請求(revalidate),拿到新資料後再更新畫面。
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then(r => r.json());
function UserProfile({ id }) {
const { data, error, isLoading } = useSWR(`/api/users/${id}`, fetcher);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error!</p>;
return <h1>{data.name}</h1>;
}API 更簡單,適合不需要太多進階功能的專案。
React Query vs SWR vs 手動管理
| 特性 | React Query | SWR | 手動管理 |
|---|---|---|---|
| 自動快取 | ✅ | ✅ | ❌(自己寫) |
| 自動重新請求 | ✅ | ✅ | ❌ |
| Devtools | ✅ | ❌ | ❌ |
| Mutation 管理 | ✅(完整) | 有限 | ❌ |
| 離線支援 | ✅ | ❌ | ❌ |
| 學習曲線 | 中等 | 低 | 低(但後續維護成本高) |
| Bundle 大小 | ~13KB | ~4KB | 0 |
一句話結論: 如果你的 app 有超過 3 個以上的 API 呼叫,強烈建議用 React Query 或 SWR,不要自己管 loading state。你省下來的時間和少寫的 bug 絕對值得多裝一個套件。
資料傳遞格式
Query String
// GET 請求參數
fetch('/api/users?page=1&limit=10&sort=name');
// 使用 URLSearchParams
const params = new URLSearchParams({
page: 1,
limit: 10,
sort: 'name'
});
fetch(`/api/users?${params}`);JSON Body
fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'John', age: 25 })
});FormData
// 表單資料(含檔案上傳)
const formData = new FormData();
formData.append('name', 'John');
formData.append('avatar', fileInput.files[0]);
fetch('/api/users', {
method: 'POST',
body: formData // 不需要設定 Content-Type
});實務封裝範例
// api.js - 統一封裝
const api = {
baseURL: '/api',
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
};
// 自動帶 Token
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
const response = await fetch(url, config);
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'API Error');
}
return response.json();
},
get(endpoint) {
return this.request(endpoint);
},
post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data),
});
},
};
// 使用
const users = await api.get('/users');
const newUser = await api.post('/users', { name: 'John' });怎麼選?
| 場景 | 推薦方案 |
|---|---|
| 簡單的一次性請求 | fetch(原生、零依賴) |
| 需要攔截器/統一錯誤處理 | axios |
| React/Vue 專案的資料請求 | React Query / SWR |
| 檔案上傳需要進度條 | axios(或 XHR) |
| SSR / Server Component | 直接用 fetch(Next.js 原生支援) |
延伸閱讀
MDN - Fetch API Axios 官方文件 TanStack Query 官方文件 SWR 官方文件 RESTFul API API CRUD