第 2 週 · API 設計與接上前端
前端 fetch 與 CORS
用你熟悉的 fetch 呼叫 Worker,並看懂瀏覽器為什麼擋下跨網域。
正在打開這一關…
學習目標
- 用 fetch 送出 GET 與 POST,並檢查 response.ok
- 說明 CORS 是瀏覽器的規則,不是用 curl 測得出來的鎖
- 讓本機網頁與本機 Worker 在不同埠時仍能對話
你會 JavaScript。從網頁拿資料,就是 fetch。今天把網頁與 Worker 接上。最容易卡住的不是語法,是瀏覽器的同源政策:網頁在 http://localhost:5173,API 在 http://localhost:8787,埠不同就算不同來源。
用生活例子理解
同源指的是協定、主機、埠都一樣。https://shop.example 與 https://api.example 是不同來源。瀏覽器預設不讓網頁讀取另一個來源的回應,免得惡意網頁偷看你已經登入的銀行。
CORS(跨來源資源共享)是伺服器用標頭說:「這個來源的網頁可以讀我的回應。」瀏覽器看到才放行。
生活比喻:CORS 像美術館的留言「允許某某學校的校車進來」。鎖不是掛在 API 伺服器的門上(
curl沒有這道鎖,所以curl會成功),鎖在瀏覽器這位保全身上。
所以:
- 終端機的
curl成功,不代表網頁的fetch會成功。 - 正式環境若網頁與 API 同一個網域,常常不必為了讀 JSON 特別開 CORS。期末專案會把兩者放在一起。今天故意分開,把問題看清楚。
- 不要用
Access-Control-Allow-Origin: *再搭配會帶 cookie 的請求。本課先不帶 cookie,只用一個明確的來源。
帶 JSON 的 POST 會先發一筆 預檢 OPTIONS。你要對 OPTIONS 回 204,並帶上允許的方法與標頭。
動手做
大約 25 分鐘。
- Worker 用今天的案例,
npm run dev,確認它在8787。 - 另開一個資料夾,放
index.html,用任何靜態伺服器打開。你可以用npx serve(多半是3000或5173)。重點是埠與8787不同。 - 把案例裡的
ALLOWED改成你的網頁來源,含協定與埠,沒有路徑結尾。 - 打開網頁,按「讀取」與「送出」。Network 應看到
GET或POST,狀態是 200 或 201。 - 把允許的來源改錯一個埠,重新整理。主控台會出現 CORS 錯誤。把埠改回來。
- 在 Network 找
OPTIONS。它應該是204。
案例
const messages = [];
function corsHeaders(env) {
return {
"access-control-allow-origin": env.ALLOWED_ORIGIN,
"access-control-allow-methods": "GET, POST, OPTIONS",
"access-control-allow-headers": "Content-Type",
vary: "Origin",
};
}
export default {
async fetch(request, env) {
if (request.method === "OPTIONS") {
return new Response(null, { status: 204, headers: corsHeaders(env) });
}
const url = new URL(request.url);
if (url.pathname !== "/api/messages") {
return Response.json({ error: "找不到" }, { status: 404, headers: corsHeaders(env) });
}
if (request.method === "GET") {
return Response.json(messages, { headers: corsHeaders(env) });
}
return Response.json({ error: "不支援" }, { status: 405, headers: corsHeaders(env) });
},
};{
"name": "guestbook-cors",
"main": "src/index.js",
"compatibility_date": "2026-10-30",
"vars": {
"ALLOWED_ORIGIN": "http://localhost:3000"
}
}vars 是非機密的設定,會出現在設定檔裡。允許的網址不是密碼,可以放這裡。密碼不能。
<!doctype html>
<meta charset="utf-8">
<button id="load" type="button">讀取留言</button>
<pre id="out"></pre>
<script type="module">
const endpoint = "http://localhost:8787/api/messages";
const out = document.querySelector("#out");
document.querySelector("#load").addEventListener("click", async () => {
const response = await fetch(endpoint);
if (!response.ok) {
out.textContent = `失敗 ${response.status}`;
return;
}
const data = await response.json();
out.textContent = JSON.stringify(data, null, 2);
});
</script>POST 的寫法你已經會:fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ author, body }) })。記得先看 response.ok。
常見錯誤
- 只在伺服器回 JSON,沒有 CORS 標頭,然後一直檢查 Worker 的
console.log。瀏覽器在進到你的response.json()之前就把回應丟掉了。 - 允許的來源寫成
http://localhost:3000/(多了斜線)或漏掉http://。要比對整串來源。 - 用
*當允許來源,之後又想帶登入 cookie。瀏覽器會拒絕。要帶 cookie 時,來源必須寫死一個網址,並且另外設定access-control-allow-credentials: true。本課先不走那條路。 - 忘記回
OPTIONS。預檢失敗時,後面的 POST 不會發出。
自我檢查
- 為什麼
curl成功,網頁仍可能失敗? OPTIONS在這個流程裡是誰發出的?你該回什麼?- 什麼樣的網址可以放在
vars,什麼不行?