Learning Objectives
- 寫出 HTML 表單,並說明 method、action、name 的作用
- 在同一個路由處理 GET(顯示表單)與 POST(處理資料)
- 說明為什麼伺服器端驗證不可以省略,並在錯誤時保留使用者已填的內容
- 說明 Post/Redirect/Get 解決的問題並實作
- 用 flash 與 get_flashed_messages 顯示一次性的訊息
表單是使用者把資料交給伺服器的方式
報名、留言、登入,都是使用者填資料、送給伺服器。第 4 課看過伺服器怎麼讀 request.form,這一課處理前後的整個流程:畫出表單、檢查內容、出錯時讓使用者修改、成功後把頁面帶開。
<form method="post" action="/new">
<input name="title">
<button>送出</button>
</form>
| 屬性 | 作用 |
|---|---|
method |
用哪個 HTTP 方法送出。沒有寫時是 GET,資料會放在網址上;會改變資料的動作用 post |
action |
送到哪個網址。沒有寫時送回目前的網址 |
name |
欄位的名稱,也就是 request.form 裡的鍵。沒有 name 的欄位不會被送出 |
HTML 的細節可以參考 HTML 與 CSS 軌。
逐步看:同一個路由處理顯示與送出
/new 同時接受 GET 與 POST。程式送出四個請求:打開表單、送空白、送太長、送正確內容。模板 form.html 裡的輸入框有 required 和 maxlength,請看伺服器有沒有因此少做檢查。
前端驗證和伺服器端驗證的分工
HTML 的 required、maxlength、type="email" 這些屬性叫做前端驗證。它們的作用是讓使用者馬上知道哪裡填錯,不是安全機制。
前端驗證在使用者的瀏覽器裡執行,而瀏覽器歸使用者控制:可以用開發者工具把屬性拿掉,也可以不透過瀏覽器,直接用程式送出請求。MDN 明白寫著:前端驗證很容易繞過,伺服器一定要再檢查一次。
| 前端驗證 | 伺服器端驗證 | |
|---|---|---|
| 目的 | 使用體驗 | 保護資料 |
| 能不能繞過 | 能 | 不能 |
| 可以省略嗎 | 可以,但使用者會不方便 | 不可以 |
欄位很多時,每個都手寫檢查會很冗長,可以看看 WTForms 這類套件。這一課先手寫,了解它們在做什麼。
重新整理為什麼會重複送出
瀏覽器按「重新整理」的意思是把最後一個請求再送一次。如果最後一個請求是 POST,就會重新送出表單:多一則留言、多扣一次款。瀏覽器會跳出確認視窗,但多數人看不懂,就直接按了確定。
解法是 Post/Redirect/Get(PRG):
- 瀏覽器送出 POST,伺服器處理資料
- 伺服器不回傳頁面,改回傳轉址(
redirect,狀態碼 302) - 瀏覽器自動送出 GET 打開新網址
這樣瀏覽器的最後一個請求是 GET,重新整理只是再讀一次頁面。
302 的規格沒有說方法一定要變,但瀏覽器收到 POST 的 302 回應時,下一個請求都會用 GET。要明確表達這個意思可以用 redirect(url, code=303),兩者在這裡的效果相同。
驗證失敗時不用轉址:要把錯誤訊息和使用者填的內容畫回表單,這時資料沒有寫入,重新送出也沒有副作用。
逐步看:兩種寫法,重新整理的結果
/bad 處理完直接回傳內容,/good 回傳轉址。程式用「再送一次最後的請求」來模擬按重新整理。
逐步看:flash 把訊息帶到下一頁
轉址之後是一個新的請求,view 函數裡的變數都不見了,那要怎麼告訴使用者「已送出」?flash 把訊息先存起來,下一個請求讀一次就清掉。
動手做:課程回饋表單
完成 /feedback 的 POST 處理,順序是:
title是空的:error = '請填寫標題'score不是 1 到 5 的整數:error = '分數要是 1 到 5'- 都沒問題:把
(title, int(score))加進items,flash('已收到'),轉址到/thanks
程式最下面的測試不要改。注意 score 讀進來是字串,可能根本不是數字。
想一想
下面的程式會印出什麼?
from flask import Flask, flash, get_flashed_messages
app = Flask(__name__)
app.secret_key = "dev"
@app.route("/save")
def save():
flash("好")
return ""
@app.route("/read")
def read():
return str(len(get_flashed_messages()))
c = app.test_client()
c.get("/save")
print(c.get("/read").text, c.get("/read").text)
沒有設定 secret_key 就呼叫 flash,會丟出什麼例外?(這段程式把 app.testing 設成 True,讓例外傳出來)
from flask import Flask, flash
app = Flask(__name__)
app.testing = True
@app.route("/")
def index():
flash("好")
return ""
app.test_client().get("/")
表單的輸入框已經寫了 required,伺服器還需要檢查欄位是不是空的嗎?
使用者送出留言後,伺服器回傳 redirect('/')。接著使用者按重新整理,瀏覽器會送出什麼?
<input type="text" placeholder="姓名"> 放在表單裡,送出後 request.form 會有這個欄位嗎?
重點整理
- 表單的
name就是request.form的鍵;會改資料的動作用method="post" - 同一個路由用
request.method分開 GET 與 POST - 前端驗證是為了使用體驗,伺服器端驗證才是保護;出錯時把錯誤與使用者填的內容畫回去
- 成功後回傳轉址(PRG),重新整理才不會重複送出
flash把訊息帶到下一個請求,讀過一次就清掉;需要secret_key,訊息不要太長,因為它存在 Cookie 裡
現在資料還放在串列裡,重新啟動伺服器就不見了。下一課接上資料庫。