JavaScript – Fetch 與網路請求(Networking)
主題:REST API 呼叫
簡介
在前端開發中,與後端服務溝通是最常見的任務之一,而 REST API 幾乎是所有現代 Web 應用的資料交換標準。透過 HTTP 請求,我們可以取得、建立、更新或刪除資源,讓前端畫面即時呈現最新的資訊。
自從 XMLHttpRequest 被 fetch 取代後,JavaScript 在處理網路請求方面變得更簡潔、更具可讀性,也更容易與 Promise、async/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-Type、Authorization 等標頭。Headers 物件可以在 fetch 的 init 參數裡設定:
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 |
最佳實踐
- 統一錯誤處理:建立一個封裝
fetch的工具函式,內部統一檢查response.ok、解析 JSON,並拋出自訂錯誤。 - 使用
async/await:保持程式碼線性,讓錯誤捕捉更直觀。 - 設定合理的 Timeout:避免使用者等待過久。
- 在開發環境使用 Mock Server:如
json-server、msw(Mock Service Worker),減少對真實 API 的依賴。 - 保持 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 取得資料的核心工具,配合 Promise 與 async/await 能寫出簡潔且易除錯的程式。- 了解 REST API 常用的 HTTP 方法、標頭設定以及 錯誤處理(
response.ok、AbortController)是成功呼叫 API 的關鍵。 - 最佳實踐 包括統一封裝
fetch、設定合理的 timeout、避免跨域問題、以及將授權資訊安全地管理。 - 透過上述範例,你可以快速實作 登入驗證、分頁載入、檔案上傳、國際化設定檔 等常見需求,為前端專案奠定穩固的資料交換基礎。
掌握了這些概念與技巧,你就能在任何 JavaScript 專案中自如地與後端服務互動,讓使用者體驗更即時、更流暢。祝開發順利,持續寫出優雅的程式碼!