Flask 快速指南

安裝

在 Linux,並且 Python 3.7 已經安裝好且也把 Pipenv 也裝好的情況下,建一個專案資料夾 Flask-201903,進入專案資料夾。

安裝 Flask 套件:

> pipenv install Flask

Hello, World!

照慣例來個 Hello, World!。拿編輯器再專案資料夾內開個 hello.py 檔,內容如下:

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
return 'Hello, World!'

注意大小寫別打錯了!

切換到終端機,先加個環境變數讓 Flask run 的時候知道要去跑這支腳本:

(flask-201903) > export FLASK_APP=hello.py
(flask-201903) > flask run
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

終端機提示符號前面多了那串 (flask-201903) 表示該 shell 是位於 Python 虛擬環境內。

可以看到 Flask 有提示網址以及 CTRL+C 可以退出的字樣。瀏覽器打開 127.0.0.1:5000 應該就可以看到那親切的 Hello World。

除錯模式

在開發環境開啟除錯模式會觸發下列行為:

  • 啟動 debugger
  • 啟動自動重載
  • 啟動 Flask 的除錯模式

如果沒有自動重載,每次改完程式還要手動重載,很不方便。

一樣透過設定環境變數來開啟除錯模式,再跑一波 Flask:

(flask-201903) > export FLASK_ENV=development
(flask-201903) > 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 資料

客戶端傳來的資料都放在 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() 方法,要中斷並報錯用 about() 方法:

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 錯誤頁。

Response

View 的回傳值都會被自動轉換成 response 物件,如果原本是回傳字串,則字串內容會被包成 response body,再加上 200 的 HTTP 狀態碼,與 text/html 的 mimetype。

具體的轉換邏輯如下:

  1. 如果原本就是回傳 response 物件,則就原樣回傳,不經過轉換加工。
  2. 如果是字串,會被轉換成 response 物件,加上預設參數。
  3. 如果是 tuple,則依照特定格式轉換成 response 物件:(response, status, headers) 或 (response, headers) 裡面的 status 或 headers 值會變成 response 物件的屬性,其中 header 是一個 list 或 dictionary,裡面的欄位就會是 HTTP header 的資訊。
  4. 如果以上皆非,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

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() 方法來讀出訊息。

Log 紀錄

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')

Comments