National Central University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try an AI Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

3

Local AI

--

Uedu Code

--

中央大學 AQI 80 24°C PM2.5 21
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 13

前端呼叫 API

用 fetch 取資料、更新畫面,同源政策與 CORS
Interaction實作 30 minute(s)

Learning Objectives

  • 說明整頁重新載入與部分更新的差別
  • 用 fetch、await、response.ok、response.json() 呼叫 API 並處理錯誤
  • 用 textContent 更新畫面,說明為什麼不要用 innerHTML 接伺服器資料
  • 說明同源政策、CORS 與預檢,以及 CORS 為什麼不是權限機制

整頁重新載入與部分更新

第 10 課的留言板每新增一則,瀏覽器就送一次表單、收到轉址、重新載入一整頁。頁面捲動的位置、其他輸入框的內容都會重來。

另一種做法是讓頁面裡的 JavaScript 自己呼叫 API,拿到 JSON 之後只改畫面的一小塊:

整頁重新載入 部分更新
伺服器回傳 一整頁 HTML JSON
誰決定畫面 伺服器(模板) 瀏覽器(JavaScript)
輸入框、捲動位置 重來 保留
不開 JavaScript 時 可以用 不能用

兩種都是正常的做法,同一個網站也常常混用。這種做法早期叫 AJAX,現在瀏覽器內建的工具是 fetch。這一課用到的 JavaScript 語法如果想補強,可以看 JavaScript 軌。

基本寫法:

const response = await fetch('/api/notes', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ text: '期中考範圍' }),
});
const data = await response.json();

if (!response.ok) {
  showError(data.error);
  return;
}
appendNote(data);
  • await 表示等請求完成再往下。等待的時候頁面不會卡住,使用者還能操作
  • fetch 預設是 GET。要送 JSON 時,方法、Content-Type、JSON.stringify 三樣都要有;少了 Content-Type,Flask 的 get_json() 會回 415
  • response.ok 在狀態碼是 200 到 299 時為真

最容易踩到的一點:fetch 收到 400、404、500 時不會跳到 catch。 依 MDN 的說明,只要伺服器有回應,fetch 就算成功;catch 只接得到連不上伺服器這類錯誤。所以一定要自己檢查 response.ok。

逐步看:同一支 API 成功與失敗

逐步動畫只能追蹤 Python,所以這裡用 Python 寫出和上面相同的流程,call 這個函數就是前端的角色。對照方式:

JavaScript 這個範例
await fetch(…) app.test_client().post(…)
response.ok ok
await response.json() response.get_json()

互動練習:不換頁的筆記

按「啟動伺服器」。static/app.js 是這一頁的 JavaScript。新增筆記時注意兩件事:

  • 下面的「請求紀錄」只多一個 POST,頁面沒有重新載入
  • 程式還少了 response.ok 的檢查,任務會帶你看到後果再補上

更新畫面用 textContent

剛才的練習裡,同一筆資料只差一個屬性,結果就差很多:

li.textContent = note.text;    // 當成文字,標籤會原樣顯示
li.innerHTML = note.text;      // 當成 HTML,裡面的標籤與事件都會生效

第 6 課的 Jinja2 會自動跳脫,但 JavaScript 自己組畫面時沒有人幫你跳脫。如果別的同學存進一則帶 onerror 的筆記,所有打開這頁的人都會執行那段程式,這就是 XSS(跨站腳本攻擊),第 17 課會再談。

所以規則很簡單:伺服器來的資料一律用 textContent,或用 createElement 一個一個組。真的需要顯示使用者寫的 HTML(例如 Markdown 轉出來的內容)時,要先經過 DOMPurify 這類專門的清理工具。

同源政策與 CORS

到目前為止,頁面和 API 都在同一個網站。如果 https://other.example 這一頁的程式去呼叫你的 API 呢?

想像你登入了學校的系統,又開了一個不認識的網站。如果那個網站的程式可以隨意讀取學校系統的回應,它就能看到你的成績。所以瀏覽器有 同源政策(same-origin policy):頁面的程式只能讀取同一個來源的回應。

來源(origin)由通訊協定、主機、埠號三者組成,三個都相同才算同源。下面第一段動畫逐一比較。

有時候跨來源是你要的,例如前端和 API 放在不同的網域。CORS(Cross-Origin Resource Sharing)讓伺服器用回應標頭告訴瀏覽器:這個來源可以讀。

Access-Control-Allow-Origin: https://other.example

跨來源的請求分成兩種:

條件(節錄) 瀏覽器怎麼做
簡單請求 GET、HEAD、POST;Content-Type 只能是表單類或 text/plain;沒有自訂標頭 直接送出,再看回應標頭決定要不要給頁面讀
需要預檢 其他情況,例如 Content-Type: application/json、PUT、DELETE 先送 OPTIONS 問伺服器,允許了才送真正的請求

注意第一列:請求已經送到伺服器了,瀏覽器擋的是頁面讀取回應。第二段動畫從伺服器的角度看一次。

逐步看:哪些網址算同源

左邊都是同一頁,右邊是它想呼叫的網址。第 2 個為什麼是同源?第 4 個的主機只差一點點,算嗎?

逐步看:從伺服器的角度

每個請求都帶 Origin 標頭,就像瀏覽器從別的網站送來的一樣。view 函數有沒有檢查它?按讚的請求有沒有被處理?

互動練習:從另一個網站呼叫

按工具列的「開另一個網站」,畫面會換成 https://other.example 的一頁,它的程式會呼叫這裡的 API。每按一個按鈕,都看一下這一頁顯示什麼,再到「請求紀錄」和「伺服器紀錄」比較伺服器那邊發生了什麼。

下面的 API 工具不是瀏覽器裡的程式,作用和 curl 相同。

模擬瀏覽器依 CORS 規則檢查預檢回應:Access-Control-Allow-Origin 之外,還要以 Access-Control-Allow-Methods、Access-Control-Allow-Headers 允許對應的方法與標頭。檢查結果與原因寫在伺服器紀錄。

CORS 不是權限機制

剛才的練習可以整理成三點:

  1. CORS 是瀏覽器在執行的規則。 curl、Python 的 requests、手機 App 都不管它,所以不能靠它保護 API。誰可以讀寫資料,要由伺服器檢查登入與權限(第 15、16 課)。
  2. 簡單請求還是會到伺服器。 沒有 Access-Control-Allow-Origin 只代表頁面讀不到回應,資料可能已經改了。這和第 17 課的 CSRF 直接相關。
  3. CORS 是放寬,不是加強。 同源政策本來就擋著,CORS 是在上面開洞,所以只開給需要的來源。

幾個實務上的細節:

  • Access-Control-Allow-Origin: * 表示任何來源都能讀,適合公開、不需要登入的資料。帶 Cookie 的請求不能用 *,必須寫明來源,還要加上 Access-Control-Allow-Credentials: true,前端也要設 credentials: 'include'
  • 要開放給多個來源時,先把 Origin 和白名單比對,符合才回傳那個來源,並加上 Vary: Origin(下面的實作題)
  • 實際專案常用 Flask-CORS 這類擴充處理預檢,原理和這裡相同

動手做:CORS 白名單

完成 add_cors:Origin 在白名單裡時,加上 Access-Control-Allow-Origin,值就是那個來源;所有回應都加 Vary: Origin,告訴快取「回應會依 Origin 不同」。程式最下面的測試不要改。

你的輸出
預期輸出

想一想

1.

API 回了 404,下面的程式會顯示什麼?

try {
  const response = await fetch('/api/notes/99');
  const data = await response.json();
  show('成功:' + data.error);
} catch (err) {
  show('失敗');
}

API 的 404 回應內容是 {"error": "找不到"}。

2.

要把伺服器回傳的筆記內容放進畫面,哪一種寫法才不會執行內容裡的程式?

3.

頁面在 https://note.example.tw,下面哪個網址和它同源?

4.

另一個網站的程式對你的 API 送了 fetch(url, {method: "POST"}),你的回應沒有任何 CORS 標頭。下面哪一句正確?

5.

同學說:「我們的 API 設了 CORS 白名單,所以只有自己的前端能呼叫,不用再檢查登入了。」哪裡有問題?

第 1 次作答

重點整理

  • fetch 讓頁面只更新一部分;送 JSON 時方法、Content-Type、JSON.stringify 都要有
  • fetch 收到 400、500 不會跳到 catch,一定要檢查 response.ok
  • 伺服器來的資料用 textContent 放進畫面,不要用 innerHTML
  • 同源=通訊協定、主機、埠號都相同;同源政策限制的是頁面讀取回應
  • CORS 讓伺服器開放給指定的來源;簡單請求還是會到伺服器,其他請求會先預檢
  • CORS 不是權限機制,誰能讀寫資料要由伺服器檢查

上面一直提到「登入」,可是 HTTP 本身不記得誰是誰。下一課看瀏覽器和伺服器怎麼靠 Cookie 與 Session 記住狀態。

這一課完成了!
Next lesson:Cookie 與 Session
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。