本文 AI 產出,尚未審核

JavaScript – Fetch 與網路請求(Networking)

主題:REST API 呼叫


簡介

在前端開發中,與後端服務溝通是最常見的任務之一,而 REST API 幾乎是所有現代 Web 應用的資料交換標準。透過 HTTP 請求,我們可以取得、建立、更新或刪除資源,讓前端畫面即時呈現最新的資訊。

自從 XMLHttpRequestfetch 取代後,JavaScript 在處理網路請求方面變得更簡潔、更具可讀性,也更容易與 Promiseasync/await 搭配使用。掌握 fetch 的正確用法,不僅能提升開發效率,還能寫出更易除錯、可維護的程式碼。

本篇文章將從核心概念出發,示範多種 REST API 呼叫 的實作方式,並說明常見的陷阱與最佳實踐,最後提供幾個實務應用情境,幫助你在真實專案中快速上手。


核心概念

1. fetch 基本語法

fetch 接受兩個參數:URL(字串)與設定物件(可選),回傳一個 Promise,最終解析為 Response 物件。最簡單的 GET 請求如下:

fetch('https://api.example.com/users')
  .then(response => {
    // 先檢查 HTTP 狀態碼
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    // 解析 JSON
    return response.json();
  })
  .then(data => {
    console.log('使用者資料:', data);
  })
  .catch(err => {
    console.error('取得資料失敗:', err);
  });

重點response.ok 只會在狀態碼在 200–299 之間回傳 true,因此必須自行檢查錯誤。


2. 使用 async/await 重構

async/await 讓非同步流程看起來像同步程式碼,提升可讀性:

async function getUsers() {
  try {
    const response = await fetch('https://api.example.com/users');
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const users = await response.json();
    console.log('使用者資料:', users);
  } catch (error) {
    console.error('取得資料失敗:', error);
  }
}

getUsers();

技巧:將錯誤處理集中於 try…catch 區塊,可避免每個 .then 都寫 if (!response.ok)


3. 常見 HTTP 方法

方法 說明 範例
GET 取得資源(不帶 body) fetch(url)
POST 建立新資源,需傳送 JSON 或表單資料 fetch(url, { method: 'POST', body: JSON.stringify(payload) })
PUT 完全取代資源 fetch(url, { method: 'PUT', body: JSON.stringify(payload) })
PATCH 部分更新資源 fetch(url, { method: 'PATCH', body: JSON.stringify(payload) })
DELETE 刪除資源 fetch(url, { method: 'DELETE' })

4. 設定請求標頭(Headers)

REST API 常需要 Content-TypeAuthorization 等標頭。Headers 物件可以在 fetchinit 參數裡設定:

const token = 'Bearer abcdef123456';
const headers = new Headers({
  'Content-Type': 'application/json',
  'Authorization': token,
});

fetch('https://api.example.com/posts', {
  method: 'POST',
  headers,
  body: JSON.stringify({
    title: 'My first post',
    content: 'Hello, world!',
  }),
})
  .then(res => res.json())
  .then(data => console.log('新增貼文成功:', data))
  .catch(err => console.error('錯誤:', err));

提醒:若使用 FormData 物件,不要 手動設定 Content-Type,瀏覽器會自動加上正確的 multipart/form-data 邊界字串。


5. 處理錯誤與超時

fetch 本身不會因為 HTTP 錯誤碼(如 404、500)而 reject Promise,必須自行檢查 response.ok。另外,fetch 也沒有內建 timeout 機制,常用 AbortController 來實作:

async function fetchWithTimeout(url, options = {}, timeout = 5000) {
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), timeout);
  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal,
    });
    clearTimeout(id);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return await response.json();
  } catch (err) {
    if (err.name === 'AbortError') {
      throw new Error('請求逾時');
    }
    throw err;
  }
}

// 使用範例
fetchWithTimeout('https://api.example.com/slow-endpoint')
  .then(data => console.log(data))
  .catch(err => console.error('失敗:', err));

6. 串接多個 API:Promise.all

有時需要同時取得多筆資料(例如同時抓取使用者與其貼文),Promise.all 能讓多個 fetch 並行執行,等待全部完成後再處理:

async function getUserAndPosts(userId) {
  const userPromise = fetch(`https://api.example.com/users/${userId}`).then(r => r.json());
  const postsPromise = fetch(`https://api.example.com/users/${userId}/posts`).then(r => r.json());

  const [user, posts] = await Promise.all([userPromise, postsPromise]);
  return { user, posts };
}

getUserAndPosts(1)
  .then(({ user, posts }) => {
    console.log('使用者:', user);
    console.log('貼文:', posts);
  })
  .catch(err => console.error('錯誤:', err));

小技巧:若其中一個請求失敗,Promise.all 會立即 reject,適合用在「全部成功才有意義」的情境。


常見陷阱與最佳實踐

陷阱 說明 解決方案
忘記檢查 response.ok 只看 fetch 是否 resolve,卻忽略 4xx/5xx 錯誤 在每次呼叫後先判斷 if (!response.ok) throw new Error…
直接使用 response.json() 而不等候 response.json() 本身回傳 Promise,若不 await 會得到未解析的 Promise 物件 使用 await response.json() 或在 .then 鏈中返回 response.json()
跨域(CORS)錯誤 前端直接呼叫不同來源的 API,瀏覽器阻擋 確認伺服器有正確設定 Access-Control-Allow-Origin,或使用 代理伺服器
忘記設定 Content-Type POST/PUT/PATCH 時伺服器無法正確解析 JSON 設定 'Content-Type': 'application/json'(若使用 FormData 則不需設定)
未處理請求逾時 網路不穩或伺服器回應慢,使用者會一直等下去 使用 AbortController 或自行實作 timeout 機制
過度嵌套 .then 形成「回呼地獄」 async/await 重構,或使用 Promise 鏈的方式保持平坦結構
未對敏感資料加密或隱藏 把 API 金鑰直接寫在前端程式碼 把金鑰放在後端或使用環境變數,前端只傳遞 JWT 等授權 token

最佳實踐

  1. 統一錯誤處理:建立一個封裝 fetch 的工具函式,內部統一檢查 response.ok、解析 JSON,並拋出自訂錯誤。
  2. 使用 async/await:保持程式碼線性,讓錯誤捕捉更直觀。
  3. 設定合理的 Timeout:避免使用者等待過久。
  4. 在開發環境使用 Mock Server:如 json-servermsw(Mock Service Worker),減少對真實 API 的依賴。
  5. 保持 API 呼叫的可重用性:將 URL、Headers、body 等抽離成參數或配置檔,方便在不同模組間共享。

實際應用場景

1. 建立登入流程(JWT)

async function login(username, password) {
  const response = await fetch('https://api.example.com/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username, password }),
  });

  if (!response.ok) {
    const err = await response.json();
    throw new Error(err.message || '登入失敗');
  }

  const { token } = await response.json();
  // 儲存 token(建議使用 HttpOnly cookie 或 localStorage + XSS 防護)
  localStorage.setItem('authToken', token);
  return token;
}

登入成功後,所有後續 API 呼叫只要在 Headers 中加入 Authorization: Bearer <token> 即可。

2. 分頁載入資料(Infinite Scroll)

let currentPage = 1;
const pageSize = 20;
const listContainer = document.getElementById('list');

async function loadMore() {
  const url = `https://api.example.com/articles?page=${currentPage}&size=${pageSize}`;
  const { articles, total } = await fetchWithTimeout(url, {}, 8000);
  
  articles.forEach(a => {
    const li = document.createElement('li');
    li.textContent = a.title;
    listContainer.appendChild(li);
  });

  currentPage++;
  if (listContainer.children.length >= total) {
    // 已載入全部
    window.removeEventListener('scroll', onScroll);
  }
}

function onScroll() {
  if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 100) {
    loadMore().catch(err => console.error('載入失敗', err));
  }
}

window.addEventListener('scroll', onScroll);
loadMore(); // 初始載入

3. 上傳檔案(FormData)

async function uploadAvatar(file) {
  const formData = new FormData();
  formData.append('avatar', file);

  const token = localStorage.getItem('authToken');

  const response = await fetch('https://api.example.com/users/me/avatar', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}` // 不要設定 Content-Type
    },
    body: formData,
  });

  if (!response.ok) {
    throw new Error('上傳失敗');
  }
  const result = await response.json();
  console.log('上傳成功,檔案網址:', result.url);
}

4. 多語系網站的設定檔載入

async function loadLocale(locale = 'zh-TW') {
  const data = await fetchWithTimeout(`/i18n/${locale}.json`);
  // 假設使用 i18next
  i18next.init({
    lng: locale,
    resources: {
      [locale]: { translation: data }
    }
  });
}
loadLocale(); // 載入預設語系

總結

  • fetch 為現代 JavaScript 取得資料的核心工具,配合 Promiseasync/await 能寫出簡潔且易除錯的程式。
  • 了解 REST API 常用的 HTTP 方法、標頭設定以及 錯誤處理response.okAbortController)是成功呼叫 API 的關鍵。
  • 最佳實踐 包括統一封裝 fetch、設定合理的 timeout、避免跨域問題、以及將授權資訊安全地管理。
  • 透過上述範例,你可以快速實作 登入驗證、分頁載入、檔案上傳、國際化設定檔 等常見需求,為前端專案奠定穩固的資料交換基礎。

掌握了這些概念與技巧,你就能在任何 JavaScript 專案中自如地與後端服務互動,讓使用者體驗更即時、更流暢。祝開發順利,持續寫出優雅的程式碼!