cover

概念圖

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 的 useEffect cleanup 裡搭配 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

特性FetchAxios
瀏覽器原生❌(需安裝)
自動 JSON 轉換
請求攔截
請求取消AbortControllerCancelToken
錯誤處理需手動檢查自動 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 };
}

現代框架的資料請求

上面介紹的 fetchaxios 都是「發請求」的工具,但在真實的 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 QuerySWR手動管理
自動快取❌(自己寫)
自動重新請求
Devtools
Mutation 管理✅(完整)有限
離線支援
學習曲線中等低(但後續維護成本高)
Bundle 大小~13KB~4KB0

一句話結論: 如果你的 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