JAVASCRIPT V2 · BROWSER · 68
fetch / API:Browser 怎麼把遠端資料接回 State?
fetch() 不是「拿 JSON 的魔法」。它啟動 HTTP request,得到 Response,你再檢查 status、讀 body、解析 representation、更新 JavaScript state,最後 render UI。這一課要把整條 request lifecycle 看清楚。
Learning outcomes
- 畫出 event → fetch → HTTP → Response → parse → state → render。
- 區分 network rejection、HTTP error response 與 JSON parse failure。
- 理解
response.ok、status、headers、body 的角色。 - 能建立 loading / success / error UI state。
- 理解 stale response / cancellation 的基本問題。
1. Why now:Local state 不等於真實系統資料
上一課的 students array 只活在 Browser memory。真正的 app 往往需要從 API 取得資料:
User clicks Load
↓
fetch("/api/students")
↓
HTTP request
↓
server
↓
HTTP response
↓
parse JSON
↓
students state
↓
render()
每一個箭頭都是獨立 failure boundary。
2. fetch() 先得到 Response,不是直接得到 JSON object
const response =
await fetch("/api/students");
console.log(
response.status,
response.ok
);
const data =
await response.json();
Response 描述 HTTP response;json() 才把 body representation 讀取並 parse 成 JavaScript value。
3. Worked Example A:最小 GET lifecycle
async function loadStudents() {
const response =
await fetch("/api/students");
if (!response.ok) {
throw new Error(
"HTTP " + response.status
);
}
const students =
await response.json();
return students;
}
這裡有至少三個不同 failure:fetch 本身 reject、status 非 2xx、body 不是合法 JSON。
4. HTTP 404 / 500 通常不會讓 fetch 自動 reject
const response =
await fetch("/missing");
console.log(response.status); // 404
console.log(response.ok); // false
DNS/TCP/TLS/connection 等 network-level failure 才常讓 Promise reject。HTTP 500 是「server 有回應,而且 status 表示 failure」。所以只寫 catch 而不檢查 status 是不完整的。
5. Worked Example B:先看 raw body,再決定 parse
const response =
await fetch("/api/students");
const raw =
await response.text();
console.log({
status: response.status,
contentType:
response.headers.get(
"content-type"
),
raw
});
如果 server 實際回 HTML error page,硬做 response.json() 只會得到第二個錯誤,遮住原本的 server response。
6. POST JSON:request 也有 method / headers / body
const response =
await fetch(
"/api/students",
{
method: "POST",
headers: {
"Content-Type":
"application/json"
},
body: JSON.stringify({
name: "Amy",
score: 90
})
}
);
Client 能送 request,不代表 server 會接受。Validation、authentication、authorization 仍由可信 server/data layer enforce。
7. UI state 應該是一個可推理的狀態機
{
status: "idle",
data: [],
error: null
}
{
status: "loading",
data: [],
error: null
}
{
status: "success",
data: students,
error: null
}
比起散落的 isLoading、hasError、done booleans,明確 state machine 更不容易出現互相矛盾的狀態。
8. Worked Example C:Fetch → State → Render
async function refresh() {
state = {
status: "loading",
data: [],
error: null
};
render();
try {
const data =
await loadStudents();
state = {
status: "success",
data,
error: null
};
} catch (error) {
state = {
status: "error",
data: [],
error: error.message
};
}
render();
}
這裡真正重要的是每個 transition 都能被觀察與測試。
9. Response body 只能被 consume:設計 debugging 時要注意
const response =
await fetch(url);
const data =
await response.json();
// 再次讀同一 body
// 可能失敗,因為 stream 已讀取
初學階段不必深入 Streams API,但要知道 response body 不是永遠可以反覆 parse 的普通 object。
10. Stale response:後送的 request 不一定後回
search("java"); // request A
search("js"); // request B
// B 先回
// A 後回
// 如果無保護,舊 A 可能覆蓋新 B
這不是「網路很怪」,而是 concurrency ordering 問題。
11. Cancellation:AbortController
let controller;
async function search(query) {
controller?.abort();
controller =
new AbortController();
const response =
await fetch(
"/api/search?q=" +
encodeURIComponent(query),
{
signal:
controller.signal
}
);
return response.json();
}
132 會更深入 race / latest-wins;這裡先知道舊 request 可以被取消。
12. Common mistakes
A. 以為 500 一定進 catch
先檢查 response.ok/status。
B. 把 Response 當成 parsed JSON
還要讀 body。
C. Loading 永遠不結束
Error path 忘了更新 state。
D. 每次 input 都發 request,舊結果覆蓋新結果
需要 debounce/cancellation/version strategy。
13. Debug evidence:不要只看 UI 顯示「API failed」
console.log({
url: response.url,
status: response.status,
ok: response.ok,
contentType:
response.headers.get(
"content-type"
)
});
- Network request 有沒有真的送出?
- Final URL / method / status 是什麼?
- Response body 寫什麼?
- Body representation 與 Content-Type 一致嗎?
- Parse 成功後 state 是否更新?
- 最後 render 是否執行?
14. Guided exercise:完成 loadTitle(url)
async function loadTitle(url) {
const response =
await fetch(url);
// 1. parse JSON
// 2. return data.title
}
先說明 response 與 data 的 type/role,再完成本頁 executable mastery tests。
15. Independent exercise:寫一個有 HTTP check 的 loader
完成 loadCourses(url):fetch → 若 !response.ok throw HTTP n → parse JSON → 必須確認結果是 array,否則 throw Invalid payload。
16. Challenge:設計 Search latest-wins
使用者快速輸入 java、javascript、js。設計一個方案保證最後 UI 不會被較舊 request 覆蓋。可以選 AbortController 或 request version;要寫出 state transitions。
Project checkpoint:Grade Tracker Remote Data
Load button
↓ event
refresh()
↓
state = loading
↓
fetch API
↓
status + body
↓
students[]
↓
state = success/error
↓
render()
現在 Grade Tracker 從純 Browser memory 進入真正 client/server boundary。下一課要學會用 DevTools 看這條路徑每一層的 evidence。
17. Vocabulary / Summary
- request:client 送出的 HTTP message。
- response:server 回傳的 HTTP message。
- status:response 對結果的 protocol-level classification。
- body:實際 payload representation。
- parse:representation → JavaScript value。
- stale response:較舊操作晚回並覆蓋較新 state。
18. Further reading
Knowledge check
- 解釋 fetch resolve 與 HTTP success 為何不是同一件事。
- 區分 Response object 與 parsed JSON value。
- 畫出 loading → success/error state transitions。
- 說明 stale response 如何產生。
- 完成本頁 Fetch + JSON executable mastery tests。