本文 AI 產出,尚未審核
JavaScript – Fetch 與網路請求(Networking)
主題:fetch + async/await
簡介
在現代前端開發中,與伺服器交換資料是最常見的需求。無論是取得部落格文章、送出使用者表單,或是與第三方 API 串接,都離不開 HTTP 請求。
傳統上,我們會使用 XMLHttpRequest 或是第三方函式庫(如 Axios)來完成這件事,但自 ES6 起,原生的 fetch API 已經成為瀏覽器的標準,提供 更直觀、可讀性更高 的語法。
然而,fetch 本身回傳的是 Promise,若直接使用 .then()、.catch(),程式碼仍可能因多層嵌套而變得難以閱讀。async/await 正是為了讓 非同步流程 看起來像同步程式碼一樣順暢。將兩者結合,就能寫出 簡潔、易除錯且具可維護性的網路請求程式。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,一路帶你掌握 fetch + async/await 的完整使用方式,讓你在實務開發中得心應手。
核心概念
1. fetch 是什麼?
fetch(url, options)會回傳一個 Promise,在請求成功(即收到回應)時 resolve,失敗(如網路斷線)時 reject。- 回傳的 Promise 解析後得到的是
Response物件,它包含狀態碼、標頭以及實體資料(body)。 - 要真正取得資料(JSON、文字、Blob…),必須呼叫
response.json()、response.text()等方法,這些方法同樣回傳 Promise。
2. 為什麼要搭配 async/await?
async關鍵字會把函式轉成 返回 Promise 的非同步函式。- 在
async函式內,使用await可以 暫停執行,直到 Promise 完成(resolve)或失敗(reject)。 - 這樣的寫法讓 錯誤捕捉、流程控制 更直觀:只需要
try / catch就能一次處理所有錯誤。
3. 基本語法
async function getData(url) {
try {
const response = await fetch(url); // 1. 發送請求,等待回應
if (!response.ok) { // 2. 檢查 HTTP 狀態碼 (200~299)
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json(); // 3. 解析 JSON
return data; // 4. 回傳結果(仍是 Promise)
} catch (err) {
console.error('取得資料失敗:', err);
throw err; // 讓呼叫端得知錯誤
}
}
程式碼範例
以下提供 五個實用範例,從最簡單的 GET 請求,到帶有認證、上傳檔案、並行請求與取消請求的完整示範。
1️⃣ 基礎 GET 請求 + JSON 解析
// 取得 JSONPlaceholder 的文章清單
async function fetchPosts() {
const url = 'https://jsonplaceholder.typicode.com/posts';
const response = await fetch(url);
if (!response.ok) throw new Error(`Error ${response.status}`);
const posts = await response.json();
console.log('Posts:', posts);
}
fetchPosts();
說明:
await fetch(url)等待伺服器回傳Response。response.ok為true時代表狀態碼在 200~299 範圍內。await response.json()會把回傳的字串自動轉成 JavaScript 物件。
2️⃣ POST 請求 + 傳送 JSON Body
async function createPost(title, body, userId) {
const url = 'https://jsonplaceholder.typicode.com/posts';
const payload = { title, body, userId };
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json' // 告訴伺服器傳送的資料型別
},
body: JSON.stringify(payload) // 必須先序列化成字串
});
if (!response.ok) throw new Error('建立失敗');
const result = await response.json();
console.log('建立成功:', result);
}
createPost('新文章', '這是內容', 1);
重點:
method必須明確指定(預設為GET)。Content-Type: application/json讓伺服器知道我們傳送的是 JSON。JSON.stringify必須在body中使用,否則會傳送[object Object]。
3️⃣ 加入認證 Header(Bearer Token)
async function fetchProtectedResource(token) {
const url = 'https://api.example.com/user/profile';
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}` // 常見的 JWT 認證方式
}
});
if (!response.ok) throw new Error('授權失敗');
const profile = await response.json();
console.log('使用者資料:', profile);
}
Tips:
- 千萬不要 把 token 直接寫在程式碼裡,應從安全的儲存機制(如
localStorage、sessionStorage、或更安全的 HTTP‑Only cookie)取得。- 若伺服器回傳 401/403,代表 token 失效或權限不足,可在
catch中統一導向登入頁面。
4️⃣ 並行多個請求(Promise.all + await)
async function fetchDashboard() {
const urls = [
'https://api.example.com/stats',
'https://api.example.com/notifications',
'https://api.example.com/messages'
];
// 同時發送三個請求,等待全部完成
const responses = await Promise.all(urls.map(u => fetch(u)));
// 檢查每個回應狀態
responses.forEach(r => {
if (!r.ok) throw new Error(`Request failed: ${r.url}`);
});
// 解析全部 JSON
const [stats, notifications, messages] = await Promise.all(
responses.map(r => r.json())
);
console.log({ stats, notifications, messages });
}
fetchDashboard();
說明:
Promise.all會在 任一 Promise 拒絕時立即拋出錯誤,適合需要所有結果才能繼續的情境。- 若希望 部分失敗也能取得成功的結果,可以改用
Promise.allSettled。
5️⃣ 取消請求(AbortController)
async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController(); // 建立控制器
const id = setTimeout(() => controller.abort(), timeout); // 超時自動 abort
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(id); // 請求成功,取消計時器
if (!response.ok) throw new Error('Network error');
return await response.json();
} catch (err) {
if (err.name === 'AbortError') {
console.warn('請求已被取消(逾時)');
} else {
console.error('其他錯誤:', err);
}
throw err;
}
}
fetchWithTimeout('https://api.example.com/slow-endpoint', 3000);
關鍵點:
AbortController讓你可以在任何時候呼叫controller.abort(),fetch會拋出AbortError。- 常用於 超時控制、使用者切換頁面時取消未完成的請求,避免不必要的資源浪費與 UI 異常。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
忘記 await |
fetch 回傳的是 Promise,若不加 await,後續程式會在未完成前就執行。 |
確認每個需要結果的呼叫都有 await,或使用 .then() 明確鏈接。 |
只檢查 catch |
fetch 只在 網路錯誤 時 reject,HTTP 錯誤(如 404、500)仍會 resolve。 |
必須自行檢查 response.ok 或 response.status,如範例所示。 |
直接使用 response.json() 多次 |
Response 的 body 只能讀一次,重複呼叫會拋錯。 |
若需要多種格式,先 clone() 或在一次讀取後自行轉換。 |
忘記設定 Content-Type |
POST/PUT 時不設定,伺服器可能無法正確解析 JSON。 | 在 headers 中加入 'Content-Type': 'application/json'。 |
在 UI 直接呼叫 await |
在 React、Vue 等框架的渲染函式中直接 await 會阻塞 UI。 |
把非同步邏輯搬到 useEffect、mounted、或事件處理器中。 |
| 未處理跨域(CORS)問題 | 瀏覽器會因同源政策阻擋跨域請求。 | 確認伺服器有正確設定 Access-Control-Allow-Origin,或使用 代理。 |
最佳實踐
統一錯誤處理
- 建立一個封裝
fetch的工具函式,統一檢查ok、拋出自訂錯誤,讓上層只需要try / catch。
- 建立一個封裝
使用
AbortController- 為每個長時間請求加上超時或取消機制,提升使用者體驗。
緩存與重試
- 針對不常變動的資源,可在客戶端快取(
Cache-Control、localStorage),或在失敗時自動重試(指數退避)。
- 針對不常變動的資源,可在客戶端快取(
分離 API 與 UI
- 把所有網路請求寫在 service layer,讓 UI 只關心資料顯示與狀態,降低耦合度。
避免過度並行
- 同時發送太多請求會導致瀏覽器或伺服器資源耗盡。可以使用 佇列 或 節流(throttle)控制同時請求數量。
實際應用場景
| 場景 | 需求 | fetch + async/await 的解法 |
|---|---|---|
| 使用者登入 | 送出帳號密碼、取得 JWT、存入安全儲存 | POST 請求、await 解析回傳 token、localStorage.setItem |
| 商品列表分頁 | 依頁碼載入資料、滾動載入更多 | GET 請求 + AbortController 防止舊請求干擾、Promise.all 並行載入多頁 |
| 即時通知 | 定時輪詢或使用 SSE/WS 前的備援方案 | setInterval + fetch,失敗時自動重試 |
| 檔案上傳 | 大檔案分段上傳、顯示上傳進度 | fetch 搭配 ReadableStream、FormData、await 監控每段回應 |
| 多語系內容 | 依使用者語系載入不同 JSON 檔案 | Promise.allSettled 同時請求多語系檔案,失敗時回退預設語系 |
總結
fetch為瀏覽器提供的 原生 HTTP 客戶端,回傳 Promise,讓非同步操作更具可組合性。async/await把 Promise 的鏈式寫法變成 類似同步程式碼的結構,大幅提升可讀性與除錯體驗。- 真正使用
fetch時,必須自行檢查 HTTP 狀態、設定正確的 Header,並妥善處理 跨域、取消請求、錯誤重試 等情境。 - 透過 封裝、統一錯誤處理、AbortController、並行請求 等技巧,我們可以在前端應用中構建 可靠且易維護的網路層。
掌握了 fetch + async/await 後,你就能在日常開發中快速完成資料取得、表單送出、檔案上傳等任務,同時保持程式碼的簡潔與可讀。未來若需要更進階的功能(如自動重試、離線快取),只要在此基礎上擴充即可,讓你的前端專案在 效能、可靠性與使用者體驗 上都更上一層樓。祝開發順利! 🚀