23
Flask 快速指南
下文為 Flask 的〈Quickstart〉文件的摘要。
在 Linux,並且 Python 已經安裝好且也把 Pipenv 也裝好的情況下,建一個專案資料夾 flask-app,進入專案資料夾。
安裝 Flask 套件:
> pipenv install Flask
照慣例來個 Hello, World!。拿編輯器在專案資料夾內開個 hello.py 檔,內容如下:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello, World!'
注意大小寫別打錯了!
切換到終端機,先加個環境變數讓 flask run
的時候知道要去跑這支腳本:
(flask-app) > export FLASK_APP=hello.py
(flask-app) > flask run
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
終端機提示符號前面多了那串 (flask-app)
表示該 shell 是位於 Python 虛擬環境內。
可以看到 Flask 有提示網址以及 CTRL+C 可以退出的字樣。瀏覽器打開 127.0.0.1:5000 應該就可以看到那親切的 Hello World。
在開發環境開啟除錯模式會觸發下列行為:
- 啟動 debugger
- 啟動自動重載
- 啟動 Flask 的除錯模式
如果沒有自動重載,每次改完程式還要手動重載,很不方便。
一樣透過設定環境變數來開啟除錯模式,再跑一波 Flask:
(flask-app) > export FLASK_ENV=development
(flask-app) > flask run
除錯模式很方便,但也透露了很多環境訊息,在正式環境記得不要打開。
Flask 接收到某網址的請求後,會尋找該網址是否有對應的函式來做回應,這樣的動作或設計就叫路由。當代的 web app framework 都是採用路由的設計,而非真的有那個路徑的檔名存在,路由有可能是固定寫死的,也有可能寫成動態對應的,端看 app 的功能需求而定。
拿上面的 Hello World 改一下加上另一個路由:
@app.route('/')
def index():
return 'Index Page'
@app.route('/hello')
def hello():
return 'Hello, World!'
就只是很單純的靜態路由,應該很好理解。
開始來點動態路由,動態路由的範例:
@app.route('/user/<username>')
def show_user_profile(username):
# show the user profile for that user
return f'User {username}'
@app.route('/post/<int:post_id>')
def show_post(post_id):
# show the post with the given id, the id is an integer
return f'Post {post_id}'
@app.route('/path/<path:subpath>')
def show_subpath(subpath):
# show the subpath after /path/
return f'Subpath {subpath}'
傳入 app.route()
內的字串如果用角括號包住(像這樣: <variable_name>
)會被識別成變數,且不僅 app.route()
可以識別這樣的變數,後續的函式也一樣可以識別這樣的變數。
角括號內還可以轉型,參照上例,<int: post_id>
就是把客戶端請求的字串轉換成整數型態;<path: subpath>
就是把客戶端請求的字串轉換成路徑型態,完整的轉型清單如下:
- string
- int
- float
- path
- uuid
路由字串的最後,斜線或沒斜線在行為上是有所差異的:
@app.route('/projects/')
def projects():
return 'The project page'
@app.route('/about')
def about():
return 'The about page'
如果客戶端請求 projects,Flask 會自動導引到 projects/。反之如果客戶端請求 about,Flask 並不會自動導引到 about/,如果依照上面的定義,客戶端請求 about/,只會接收到 404 錯誤。如果你又很畫蛇添足的另外定義了 about/ 的路由,Flask 會報錯,並提示 URL 被重複定義的訊息。
這樣的機制確保了 URL 的唯一性,projects = projects/,about 就是 about,沒有 about/。
預設情況下,Flask 只會回應 HTTP GET 請求,如果希望 Flask 回應其它 HTTP 方法,route()
加上 methods 參數即可(注意引數名稱是複數的 methods,別拼錯了):
from flask import request
app = Flask(__name__)
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
return do_the_login()
else:
return show_the_login_form()
依照 Flask 的慣例,在專案資料夾內建立一個子目錄 static,並把常用的靜態資源像是樣式表、圖像、JavaScript 這些檔案放進去,再配合 url_for()
就可以在 Python 腳本內自動產生這些資源檔的 URL:
url_for('static', filename='style.css')
上面的例子會對應到 static/style.css。
在此之前, 路由、controller、view 都摻在一起寫,從這裡開始要引進模板的概念,Flask 使用 Jinja2 做為模板引擎,有了模板引擎,就可以在 view 內寫 view 自己的邏輯,並且幫你把變數內的 HTML 標籤過濾掉,還有就是可以達成組版的效果。
使用 render_template()
方法來使用模板:
from flask import render_template
@app.route('/hello/')
@app.route('/hello/<name>')
def hello(name=None):
return render_template('hello.html', name=name)
依照 Flask 的慣例,它會在 templates 資料夾內去找模板檔,而 templates 資料夾的位置會根據這支 web app 是模組或是套件而有所不同。
如果是模組:
/application.py
/templates
/hello.html
如果是套件:
/application
/__init__.py
/templates
/hello.html
以上面的 hello.html 為例的模板內容:
<!doctype html>
<title>Hello from Flask</title>
{% if name %}
<h1>Hello {{ name }}!</h1>
{% else %}
<h1>Hello, World!</h1>
{% endif %}
模板內還可以存取到 request
、session
、g
物件與 get_flashed_messages()
方法。
客戶端傳來的資料都放在 request
這個全域變數內,web app 利用 request
提供的資訊與客戶端互動。
客戶端的 HTTP 方法存放在 request
的 method
屬性內,而 form 就存放在 request
的 form
屬性內。示例:
from flask import request
from flask import render_template
@app.route('/login', methods=['GET', 'POST'])
def login():
error = None
if request.method == 'POST':
if valid_login(request.form['username'], request.form['password']):
return log_the_user_in(request.form['username'])
else:
error = 'Invalid username / password'
# the code below is executed if the request method was GET or the credentials were invalid
return render_template('login.html', error=error)
如果 form
屬性不存在,伺服器端會引發 KeyError
錯誤,客戶端則會收到 HTTP 400 錯誤。
如果是要取用 URL
參數,則使用 args
屬性內的 get()
方法:
searchword = request.args.get('key', '')
如果是要取用客戶端上傳的檔案,先確定在前端 HTML 表單的設定正確的屬性 enctype="multipart/form-data"
,瀏覽器才會正確的把檔案上傳。調用 request
的 files
屬性就可以調用到檔案:
from flask import request
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
f = request.files['the_file']
f.save('/var/www/uploads/uploaded_file.txt')
files
本身是個 dictionary,所以必須呼叫它裡面的 key 才會接到真正的檔案,這個 file
物件的行為就像標準的 Python file
物件,但它有個 save()
方法讓我們把檔案存到自己想要的路徑。
如果想要沿用客戶端上傳的檔名,則調用 filename
屬性,但由於檔名的不可預知,可能會有安全風險,最好用 Werkzeug 的 secure_filename()
方法過濾掉:
from flask import request
from werkzeug.utils import secure_filename
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
f = request.files['the_file']
f.save('/var/www/uploads/' + secure_filename(f.filename))
如果是要讀取 cookie 就調用 request
物件的 cookies
屬性,如果是要設置 cookie 就用 response
物件的 set_cookie()
方法。
request
的 cookies
也是一個 dictionary,裡面放了所有可以調用的 cookie。
但是如果想使用 session 的話,Flask 有提供更完善的 session 機制可以利用,不要手工用 cookie 來管理 session。
讀取 cookie 的範例:
from flask import request
@app.route('/')
def index():
username = request.cookies.get('username')
# use cookies.get(key) instead of cookies[key] to not get a KeyError if the cookie is missing.
存入 cookie 的範例:
@app.route('/')
def index():
resp = make_response(render_template(...))
resp.set_cookie('username', 'the username')
return resp
要重導使用 redirect()
方法,要中斷並報錯用 abort()
方法:
from flask import abort, redirect, url_for
@app.route('/')
def index():
return redirect(url_for('login'))
@app.route('/login')
def login():
abort(401)
this_is_never_executed()
如果不想使用 Flask 預設的陽春錯誤頁,則利用 errorhandler()
修飾子來做客製:
from flask import render_template
@app.errorhandler(404)
def page_not_found(error):
return render_template('page_not_found.html'), 404
注意到 return
那行最後面的 404
,雖然上面的修飾子已經是 404,但 return
後面還是要加 404
Flask 才認得這是 404 錯誤頁。
View 的回傳值都會被自動轉換成 response
物件,如果原本是回傳字串,則字串內容會被包成 response body,再加上 200 的 HTTP 狀態碼,與 text/html
的 mimetype
。
具體的轉換邏輯如下:
- 如果原本就是回傳
response
物件,則就原樣回傳,不經過轉換加工。 - 如果是字串,會被轉換成
response
物件,加上預設參數。 - 如果是 tuple,則依照特定格式轉換成
response
物件:(response, status, headers)
或(response, headers)
裡面的status
或headers
值會變成response
物件的屬性,其中header
是一個 list 或 dictionary,裡面的欄位就會是 HTTP header 的資訊。 - 如果以上皆非,Flask 會假設該回傳值是 WSGI 並轉換成
response
物件。
如果想要在 view 裡面先取得 response
物件,可以使用 make_response()
方法。
假設有個 view 如下:
@app.errorhandler(404)
def not_found(error):
return render_template('error.html'), 404
拿 make_response()
來幫它加工加上一組 header 資訊:
@app.errorhandler(404)
def not_found(error):
resp = make_response(render_template('error.html'), 404)
resp.headers['X-Something'] = 'A value'
return resp
Session 用來紀錄、辨識用戶的活動,實現方式是加密過的 cookie。
得先設置密鑰才能使用 session:
from flask import Flask, session, redirect, url_for, escape, request
from werkzeug.utils import secure_filename
app = Flask(__name__)
# Set the secret key to some random bytes. Keep this really secret!
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
@app.route('/')
def index():
if 'username' in session:
return f"Logged in as {escape(session['username'])}"
return 'You are not logged in'
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
session['username'] = request.form['username']
return redirect(url_for('index'))
return '''
<form method="post">
<p><input type=text name=username>
<p><input type=submit value=Login>
</form>
'''
@app.route('/logout')
def logout():
# remove the username from the session if it's there
session.pop('username', None)
return redirect(url_for('index'))
前面模板的章節有說過,模板引擎會幫我們把表單的 HTML 過濾掉,而在這裡沒有使用模板引擎,所以手動調用了 escape()
方法來濾掉 HTML 碼。
最後附註一點,瀏覽器可能會限制單一 cookie 容量,如果發現某個值應該要有卻調用不出來的話,想想看是不是超過 cookie 的容量上限了。
快閃訊息用於發送通知給用戶,例如對用戶某個行動的反應。在前一個 request
使用 flash()
方法設定訊息,在下一個 request
就可以用 get_flashed_messages()
方法來讀出訊息。
Flask app 物件有使用 Python 內建的 logger
模組,可以簡單調用:
app.logger.debug('A value for debugging')
app.logger.warning('A warning occurred (%d apples)', 42)
app.logger.error('An error occurred')
23