Learning Objectives
- 用 <name>、<int:id> 等轉換器從網址取得變數,並說明不符合時的結果
- 用 methods、@app.get、@app.post 限制 HTTP 方法,區分 404 與 405
- 說明結尾斜線的行為與路由比對的優先順序
- 用 url_for 產生網址,說明為什麼比寫死網址好
- 說明直接把使用者輸入放進 HTML 的風險,以及 escape 的作用
一個函數處理很多網址
討論區有上萬則貼文,不可能替每一則寫一個函數。實際上的做法是:網址 /note/1、/note/2⋯⋯都交給同一個函數,由網址裡的數字決定要顯示哪一則。
Flask 用角括號標出網址裡會變動的部分,比對到的值會當作引數傳給 view 函數:
@app.route('/note/<int:note_id>')
def note(note_id): # 參數名稱要和角括號裡的一樣
return f'第 {note_id} 則'
角括號裡可以寫轉換器(converter),指定接受什麼內容、傳進來是什麼型別:
| 轉換器 | 接受 | 傳入的型別 |
|---|---|---|
string(預設) |
不含斜線的文字 | str |
int |
正整數與 0 | int |
float |
帶小數點的正數 | float |
path |
和 string 一樣,但可以含斜線 |
str |
uuid |
UUID 字串 | uuid.UUID |
網址不符合轉換器時,這條路由就不算比對到,所以結果是 404,view 函數不會被呼叫。
逐步看:轉換器比對成功與失敗
程式依序請求十個網址。看哪些會跳進 view 函數,哪些直接得到 404。呼叫堆疊裡看得到傳進來的引數值和型別。
HTTP 方法與結尾斜線
路由預設只接受 GET(Flask 會順便處理 HEAD 與 OPTIONS)。要接受其他方法,用 methods 參數,或用簡寫:
@app.route('/notes', methods=['GET', 'POST']) # 同一個函數處理兩種方法
@app.get('/about') # 等於 methods=['GET']
@app.post('/vote') # 等於 methods=['POST']
網址存在但方法不對時,回應是 405 Method Not Allowed,和 404 不同:404 是沒有這個網址,405 是有這個網址,但不接受這個方法。
結尾斜線有兩種寫法,效果不一樣:
| 路由寫法 | 請求 /projects |
請求 /projects/ |
|---|---|---|
/projects/(像資料夾) |
轉址到 /projects/ |
200 |
/projects(像檔案) |
200 | 404 |
這樣同一個頁面只有一個標準網址。
逐步看:405 與結尾斜線
client.post、client.delete 可以送出不同方法的請求。注意 request.method 的值,以及回應的標頭。
url_for:用函數名稱產生網址
網頁裡到處都是連結。如果每個連結都寫死 /member/Amy,哪天網址改成 /people/Amy,就要找遍整個專案。url_for 用端點名稱(預設就是函數名稱)產生網址,只要改路由一個地方:
url_for('note', note_id=3) # /note/3
url_for('note', note_id=3, tab='reply') # /note/3?tab=reply
路由裡沒有的參數會變成查詢字串。空白、中文這類特殊字元也會自動編碼。
路由比對的順序不是看誰寫在前面:比較明確的路由優先,固定的文字優先於變數。所以 /member/me 和 /member/<name> 可以並存,請求 /member/me 一定走前者。
逐步看:url_for 的結果
url_for 要知道目前在處理哪個請求才能用。view 函數裡本來就在請求之中;這裡在函數外面,所以用 test_request_context() 製造一個。
使用者輸入不能直接放進 HTML
網址是使用者可以任意輸入的。view 函數回傳的字串預設會被瀏覽器當成 HTML,所以這樣寫有風險:
@app.route('/hi/<name>')
def hi(name):
return f'你好,{name}' # 有風險:name 裡的標籤會被瀏覽器執行
如果有人把連結做成 /hi/<img src=x onerror=...> 傳給別人,點開的人瀏覽器就會執行裡面的 JavaScript,這叫做 XSS(跨站腳本攻擊)。解決方法是用 markupsafe.escape 把特殊字元換成無害的文字:
from markupsafe import escape
return f'你好,{escape(name)}' # <b> 會變成 <b>,畫面上就是字面的 <b>
第 6 課的 Jinja2 模板預設會自動做這件事,第 17 課會完整談 XSS。
動手做:課程的三個路由
替三個函數加上路由:
course:網址/courses/<編號>,編號只接受整數join:網址/courses/<編號>/join,只接受 POST,回傳內容與狀態碼 201link:網址/courses/<編號>/link,用url_for產生course的網址,並帶上查詢字串tab=notes
程式最下面的測試不要改。
想一想
路由是 /note/<int:note_id>,請求 /note/abc 會得到什麼?
下面的程式會印出什麼?
from flask import Flask
app = Flask(__name__)
@app.post('/vote')
def vote():
return 'ok'
client = app.test_client()
print(client.get('/vote').status_code, client.get('/votes').status_code)
下面的程式會印出什麼?
from flask import Flask, url_for
app = Flask(__name__)
@app.route('/note/<int:note_id>')
def show(note_id):
return ''
with app.test_request_context():
print(url_for('show', note_id=5, page=2))
同時有 /member/<name> 和 /member/me 兩條路由,請求 /member/me 時會發生什麼?
為什麼 view 函數不應該直接回傳 f'你好,{name}'(name 來自網址)?
重點整理
<int:id>這類轉換器取出網址裡的值並轉型;不符合就是 404- 路由預設只接受 GET;
methods、@app.get、@app.post指定方法;方法不對是 405 /a/像資料夾,沒有斜線時會轉址;/a像檔案,多了斜線是 404- 固定路徑優先於變數,和書寫順序無關
- 用
url_for產生網址,改路由時連結會跟著改 - 使用者輸入放進 HTML 前要跳脫
網址能帶的資料有限。下一課看 request 物件,讀取查詢字串、表單、JSON 與標頭。