Fetch API 與 AJAX#
AJAX(Asynchronous JavaScript and XML)是在不重新整理頁面的情況下,與伺服器交換資料的技術。
例如:搜尋時即時顯示建議、送出表單後只更新部分畫面,都是 AJAX 的應用。
現代 AJAX 主要透過瀏覽器內建的 Fetch API 實作,取代了舊式的 XMLHttpRequest。
基本用法#
fetch() 接收一個 URL,回傳 Promise。需要兩個 await 才能取得資料:
1
2
3
4
5
6
7
| const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
// 第一個 await:等待伺服器回應(HTTP 標頭回來了,但 body 還在傳輸)
const data = await response.json();
// 第二個 await:等待讀取完整的 response body,並解析成 JSON
console.log(data);
|
為什麼要兩個 await?因為 HTTP 回應分兩階段:先收到狀態碼與標頭,body 可能還在傳輸中(尤其是大型資料),response.json() 會等 body 完全接收後再解析。
HTTP 方法概覽#
Fetch API 支援所有 HTTP 方法,對應不同操作:
| 方法 | 操作 | 是否有 body |
|---|
GET | 取得資料 | 否 |
POST | 新增資料 | 是 |
PUT | 完整更新 | 是 |
PATCH | 部分更新 | 是 |
DELETE | 刪除資料 | 否 |
GET 請求#
GET 是預設方法,不需要額外設定:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| async function getPost(id) {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${id}`
);
// fetch 不會因 HTTP 4xx/5xx 自動拋錯,需手動判斷
if (!response.ok) {
throw new Error(`HTTP 錯誤:${response.status}`);
}
const post = await response.json();
return post;
}
const post = await getPost(1);
console.log(post.title);
|
POST 請求#
POST 需要指定 method、Content-Type 標頭,以及序列化後的 body:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| async function createPost(title, body) {
const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: {
"Content-Type": "application/json" // 告訴伺服器 body 是 JSON 格式
},
body: JSON.stringify({ title, body, userId: 1 }) // 物件必須序列化成字串
});
if (!response.ok) throw new Error("建立失敗");
const newPost = await response.json();
console.log("建立成功,ID:", newPost.id);
return newPost;
}
|
PUT / PATCH / DELETE#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // PUT — 完整更新(傳入所有欄位)
await fetch("/api/posts/1", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "新標題", body: "新內容" })
});
// PATCH — 部分更新(只傳要修改的欄位)
await fetch("/api/posts/1", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "只更新標題" })
});
// DELETE — 刪除(通常不需要 body)
await fetch("/api/posts/1", { method: "DELETE" });
|
標頭(Headers)是附在請求或回應上的額外資訊,常見用途包括身份驗證、指定資料格式等:
1
2
3
4
5
6
7
8
9
10
11
12
13
| const response = await fetch("/api/data", {
headers: {
"Authorization": "Bearer your-token-here", // 身份驗證 token
"Accept": "application/json", // 希望伺服器回傳 JSON
"X-Custom-Header": "value" // 自訂標頭
}
});
// 讀取回應標頭與狀態
console.log(response.status); // 200、404、500 等
console.log(response.statusText); // "OK"、"Not Found" 等
console.log(response.headers.get("Content-Type")); // 回應的資料格式
console.log(response.ok); // status 200-299 時為 true
|
回應格式#
根據伺服器回傳的資料類型,選擇對應的解析方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| const response = await fetch("/api/data");
// JSON(最常用,回傳物件或陣列)
const json = await response.json();
// 純文字(HTML、CSV、純字串)
const text = await response.text();
// Blob(圖片、影片、PDF 等二進位檔案)
const blob = await response.blob();
const url = URL.createObjectURL(blob); // 產生可用於 <img src> 的暫時 URL
const img = document.createElement("img");
img.src = url;
// ArrayBuffer(需要直接操作二進位資料時)
const buffer = await response.arrayBuffer();
|
注意:response.json()、response.text() 等方法只能呼叫一次,body 讀取後就關閉了。若需要多次使用,先存入變數。
錯誤處理#
fetch 有一個常見陷阱:只有網路層錯誤(斷線、DNS 失敗)才會 reject,HTTP 404、500 等伺服器錯誤不會拋出例外,必須手動檢查:
1
2
3
4
5
6
7
8
| try {
const response = await fetch("/api/data");
console.log(response.ok); // false(即使是 404 也不拋錯)
console.log(response.status); // 404
} catch (err) {
// 這裡只有「完全無法連線」才會進來
console.error("網路錯誤");
}
|
正確的錯誤處理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| async function safeFetch(url, options = {}) {
try {
const response = await fetch(url, options);
if (!response.ok) {
// 嘗試讀取伺服器回傳的錯誤訊息
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP ${response.status}`);
}
return await response.json();
} catch (err) {
if (err.name === "TypeError") {
throw new Error("網路連線失敗,請檢查網路狀態");
}
throw err; // 重新拋出讓呼叫者處理
}
}
|
逾時(Timeout)#
Fetch 原生不支援 timeout 設定。可透過 AbortController 在指定時間後強制中止請求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
// 超過 timeout 毫秒後,呼叫 abort() 中止請求
const timer = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
signal: controller.signal // 將 signal 傳給 fetch
});
clearTimeout(timer); // 請求成功,取消計時器
return await response.json();
} catch (err) {
if (err.name === "AbortError") {
throw new Error(`請求逾時(超過 ${timeout}ms)`);
}
throw err;
}
}
|
實際範例:搜尋使用者#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| <!DOCTYPE html>
<html>
<body>
<input id="search" type="text" placeholder="輸入使用者 ID(1-10)">
<button id="btn">搜尋</button>
<div id="result"></div>
<script>
const btn = document.querySelector("#btn");
const input = document.querySelector("#search");
const result = document.querySelector("#result");
btn.addEventListener("click", async () => {
const id = input.value.trim();
if (!id) return;
result.innerHTML = "載入中...";
btn.disabled = true;
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${id}`
);
if (response.status === 404) {
result.innerHTML = "找不到此使用者";
return;
}
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const user = await response.json();
result.innerHTML = `
<h3>${user.name}</h3>
<p>Email: ${user.email}</p>
<p>網站: ${user.website}</p>
<p>城市: ${user.address.city}</p>
`;
} catch (err) {
result.innerHTML = `<span style="color:red">錯誤:${err.message}</span>`;
} finally {
btn.disabled = false; // 無論成功或失敗,都恢復按鈕
}
});
</script>
</body>
</html>
|
封裝成 API 模組#
實際專案中,通常會將 fetch 封裝成模組,統一處理 base URL、token、錯誤,避免每個地方重複相同的邏輯:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| // api.js
const BASE_URL = "https://api.example.com";
async function request(path, options = {}) {
const token = localStorage.getItem("token");
const response = await fetch(`${BASE_URL}${path}`, {
...options,
headers: {
"Content-Type": "application/json",
// 有 token 才加入 Authorization 標頭
...(token && { "Authorization": `Bearer ${token}` }),
// 允許呼叫者覆蓋標頭
...options.headers
}
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.message || `HTTP ${response.status}`);
}
return response.json();
}
export const api = {
get: (path) => request(path),
post: (path, data) => request(path, { method: "POST", body: JSON.stringify(data) }),
put: (path, data) => request(path, { method: "PUT", body: JSON.stringify(data) }),
delete: (path) => request(path, { method: "DELETE" })
};
|
1
2
3
4
5
6
| // 使用
import { api } from "./api.js";
const users = await api.get("/users");
const newUser = await api.post("/users", { name: "Alice" });
await api.delete("/users/1");
|
Reference#