Fetch API 完整指南:現代 HTTP 請求
Fetch API 是瀏覽器原生的現代 HTTP 請求介面,基於 Promise 設計,提供了比 XMLHttpRequest 更簡潔優雅的 API。
一、 基本用法
1.1 GET 請求
javascript
// 最簡單的用法
fetch("https://api.example.com/users")
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.error("Error:", error));
// async/await
async function getUsers() {
try {
const response = await fetch("https://api.example.com/users");
const data = await response.json();
console.log(data);
} catch (error) {
console.error("Error:", error);
}
}1.2 POST 請求
javascript
async function createUser(userData) {
const response = await fetch("https://api.example.com/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(userData),
});
return response.json();
}
// 使用
const newUser = await createUser({
name: "John",
email: "john@example.com",
});二、 Request 物件
2.1 完整選項
javascript
const request = new Request("https://api.example.com/users", {
method: "POST", // GET, POST, PUT, DELETE, etc.
headers: {
// Headers 物件或普通物件
"Content-Type": "application/json",
Authorization: "Bearer token123",
},
body: JSON.stringify(data), // 請求 body
mode: "cors", // cors, no-cors, same-origin
credentials: "same-origin", // omit, same-origin, include
cache: "default", // default, no-store, reload, etc.
redirect: "follow", // follow, error, manual
referrer: "about:client", // URL 或 no-referrer
referrerPolicy: "no-referrer-when-downgrade",
integrity: "", // Subresource Integrity
keepalive: false, // 頁面卸載時保持請求
signal: null, // AbortSignal
});
fetch(request);2.2 Headers 物件
javascript
// 建立 Headers
const headers = new Headers();
headers.append("Content-Type", "application/json");
headers.append("Authorization", "Bearer token");
// 或從物件建立
const headers2 = new Headers({
"Content-Type": "application/json",
Authorization: "Bearer token",
});
// 操作方法
headers.set("X-Custom", "value"); // 設定(覆蓋)
headers.get("Content-Type"); // 取得
headers.has("Authorization"); // 檢查
headers.delete("X-Custom"); // 刪除
// 迭代
for (const [key, value] of headers) {
console.log(`${key}: ${value}`);
}三、 Response 物件
3.1 Response 屬性
javascript
const response = await fetch("/api/data");
// 狀態
console.log(response.ok); // true 如果 status 200-299
console.log(response.status); // 200
console.log(response.statusText); // "OK"
// 標頭
console.log(response.headers.get("Content-Type"));
// URL
console.log(response.url); // 最終 URL(可能經過重導向)
// 類型
console.log(response.type); // basic, cors, error, opaque
// 重導向
console.log(response.redirected); // 是否經過重導向3.2 讀取 Body
Body 只能讀取一次!
javascript
const response = await fetch("/api/data");
// JSON
const json = await response.json();
// 文字
const text = await response.text();
// Blob(二進位)
const blob = await response.blob();
// ArrayBuffer
const buffer = await response.arrayBuffer();
// FormData
const formData = await response.formData();3.3 克隆 Response
如果需要多次讀取:
javascript
const response = await fetch("/api/data");
const clone = response.clone();
const json = await response.json(); // 原始 response
const text = await clone.text(); // 克隆的 response四、 錯誤處理
4.1 Fetch 的錯誤行為
WARNING
Fetch 只在網路錯誤時 reject!HTTP 錯誤狀態碼(如 404、500)不會 reject。
javascript
// ❌ 這樣不會捕捉到 404
fetch("/api/not-found")
.then((response) => response.json()) // 即使 404 也會執行
.catch((error) => console.error(error)); // 不會執行
// ✅ 正確做法
fetch("/api/data")
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.catch((error) => console.error(error));4.2 完整錯誤處理
javascript
async function fetchWithErrorHandling(url, options = {}) {
try {
const response = await fetch(url, options);
if (!response.ok) {
// 嘗試讀取錯誤訊息
let errorMessage;
try {
const errorData = await response.json();
errorMessage = errorData.message || response.statusText;
} catch {
errorMessage = response.statusText;
}
throw new Error(`HTTP ${response.status}: ${errorMessage}`);
}
return response.json();
} catch (error) {
if (error.name === "TypeError") {
// 網路錯誤
throw new Error("Network error: Unable to connect");
}
throw error;
}
}五、 常見用例
5.1 FormData 上傳
javascript
const formData = new FormData();
formData.append("name", "John");
formData.append("avatar", fileInput.files[0]);
// 不要設定 Content-Type!瀏覽器會自動設定正確的 boundary
const response = await fetch("/api/upload", {
method: "POST",
body: formData,
});5.2 URL 查詢參數
javascript
const params = new URLSearchParams({
page: 1,
limit: 10,
search: "關鍵字",
});
const response = await fetch(`/api/users?${params}`);
// /api/users?page=1&limit=10&search=%E9%97%9C%E9%8D%B5%E5%AD%975.3 設定超時
Fetch 原生不支援超時,需搭配 AbortController:
javascript
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);
return response;
} catch (error) {
clearTimeout(id);
if (error.name === "AbortError") {
throw new Error("Request timeout");
}
throw error;
}
}5.4 認證請求
javascript
// Bearer Token
const response = await fetch("/api/protected", {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
// 攜帶 Cookie(跨域)
const response = await fetch("https://api.example.com/user", {
credentials: "include",
});六、 credentials 選項
控制是否傳送 Cookie:
| 值 | 說明 |
|---|---|
'omit' | 不傳送任何 Cookie |
'same-origin' | 只有同源請求傳送(預設) |
'include' | 總是傳送,包括跨域 |
javascript
// 跨域請求需要 include
fetch("https://api.other-domain.com/user", {
credentials: "include",
});伺服器必須回應:
http
Access-Control-Allow-Origin: https://your-domain.com
Access-Control-Allow-Credentials: true七、 mode 選項
控制跨域行為:
| 值 | 說明 |
|---|---|
'cors' | 允許跨域請求(預設) |
'same-origin' | 只允許同源 |
'no-cors' | 有限的跨域存取(opaque response) |
7.1 no-cors 模式
javascript
// 可以發送請求,但無法讀取回應
const response = await fetch("https://other.com/image.png", {
mode: "no-cors",
});
console.log(response.type); // "opaque"
console.log(response.ok); // false(無法判斷)
// 無法讀取 body適用於:不需要讀取回應的場景(如埋點、預載)
八、 cache 選項
控制快取行為:
| 值 | 說明 |
|---|---|
'default' | 標準快取邏輯 |
'no-store' | 完全不使用快取 |
'reload' | 忽略快取,更新快取 |
'no-cache' | 使用快取前先驗證 |
'force-cache' | 優先使用快取 |
'only-if-cached' | 只使用快取 |
javascript
// 強制刷新
const response = await fetch("/api/data", {
cache: "no-store",
});九、 redirect 選項
控制重導向處理:
| 值 | 說明 |
|---|---|
'follow' | 自動跟隨重導向(預設) |
'error' | 遇到重導向則報錯 |
'manual' | 手動處理重導向 |
javascript
const response = await fetch("/api/redirect", {
redirect: "manual",
});
if (response.type === "opaqueredirect") {
console.log("被重導向了");
console.log(response.url); // 重導向目標 URL
}十、 封裝實踐
10.1 API 客戶端封裝
javascript
class ApiClient {
constructor(baseURL, defaultOptions = {}) {
this.baseURL = baseURL;
this.defaultOptions = defaultOptions;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
...this.defaultOptions,
...options,
headers: {
"Content-Type": "application/json",
...this.defaultOptions.headers,
...options.headers,
},
};
if (config.body && typeof config.body === "object") {
config.body = JSON.stringify(config.body);
}
const response = await fetch(url, config);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || `HTTP ${response.status}`);
}
return response.json();
}
get(endpoint, options) {
return this.request(endpoint, { ...options, method: "GET" });
}
post(endpoint, body, options) {
return this.request(endpoint, { ...options, method: "POST", body });
}
put(endpoint, body, options) {
return this.request(endpoint, { ...options, method: "PUT", body });
}
delete(endpoint, options) {
return this.request(endpoint, { ...options, method: "DELETE" });
}
}
// 使用
const api = new ApiClient("https://api.example.com", {
headers: {
Authorization: `Bearer ${token}`,
},
});
const users = await api.get("/users");
const newUser = await api.post("/users", { name: "John" });總結
| 概念 | 說明 |
|---|---|
| fetch() | 返回 Promise 的現代請求 API |
| Request | 封裝請求設定 |
| Response | 封裝回應資料 |
| Headers | 操作 HTTP 標頭 |
| Body | 多種格式:json/text/blob |
> **Fetch vs XHR**:
- Fetch 更簡潔(Promise-based)
- Fetch 無法直接追蹤進度(需要 ReadableStream)
- Fetch HTTP 錯誤不 reject
- Fetch 無法取消(需搭配 AbortController)
進階挑戰
- 實作一個支援重試機制的 Fetch 封裝(指數退避)。
- 使用 Fetch 實作請求/回應攔截器。
- 比較 no-cors 和 cors 模式的實際行為差異。