DEV Community 👩‍💻👨‍💻

Cover image for Flask 快速指南
Leon
Leon

Posted on • Originally published at editor.leonh.space

Flask 快速指南

下文為 Flask 的〈Quickstart〉文件的摘要。

安裝

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

安裝 Flask 套件:

> pipenv install Flask
Enter fullscreen mode Exit fullscreen mode

Hello, World!

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

from flask import Flask
app = Flask(__name__)

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

注意大小寫別打錯了!

切換到終端機,先加個環境變數讓 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)
Enter fullscreen mode Exit fullscreen mode

終端機提示符號前面多了那串 (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
Enter fullscreen mode Exit fullscreen mode

除錯模式很方便,但也透露了很多環境訊息,在正式環境記得不要打開。

路由

Flask 接收到某網址的請求後,會尋找該網址是否有對應的函式來做回應,這樣的動作或設計就叫路由。當代的 web app framework 都是採用路由的設計,而非真的有那個路徑的檔名存在,路由有可能是固定寫死的,也有可能寫成動態對應的,端看 app 的功能需求而定。

拿上面的 Hello World 改一下加上另一個路由:

@app.route('/')
def index():
    return 'Index Page'

@app.route('/hello')
def hello():
    return 'Hello, World!'
Enter fullscreen mode Exit fullscreen mode

就只是很單純的靜態路由,應該很好理解。

開始來點動態路由,動態路由的範例:

@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}'
Enter fullscreen mode Exit fullscreen mode

傳入 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'
Enter fullscreen mode Exit fullscreen mode

如果客戶端請求 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()
Enter fullscreen mode Exit fullscreen mode

靜態檔案

依照 Flask 的慣例,在專案資料夾內建立一個子目錄 static,並把常用的靜態資源像是樣式表、圖像、JavaScript 這些檔案放進去,再配合 url_for() 就可以在 Python 腳本內自動產生這些資源檔的 URL:

url_for('static', filename='style.css')
Enter fullscreen mode Exit fullscreen mode

上面的例子會對應到 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)
Enter fullscreen mode Exit fullscreen mode

依照 Flask 的慣例,它會在 templates 資料夾內去找模板檔,而 templates 資料夾的位置會根據這支 web app 是模組或是套件而有所不同。

如果是模組:

/application.py
/templates
    /hello.html
Enter fullscreen mode Exit fullscreen mode

如果是套件:

/application
    /__init__.py
    /templates
        /hello.html
Enter fullscreen mode Exit fullscreen mode

以上面的 hello.html 為例的模板內容:

<!doctype html>
<title>Hello from Flask</title>
{% if name %}
  <h1>Hello {{ name }}!</h1>
{% else %}
  <h1>Hello, World!</h1>
{% endif %}
Enter fullscreen mode Exit fullscreen mode

模板內還可以存取到 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)
Enter fullscreen mode Exit fullscreen mode

如果 form 屬性不存在,伺服器端會引發 KeyError 錯誤,客戶端則會收到 HTTP 400 錯誤。

如果是要取用 URL 參數,則使用 args 屬性內的 get() 方法:

searchword = request.args.get('key', '')
Enter fullscreen mode Exit fullscreen mode

如果是要取用客戶端上傳的檔案,先確定在前端 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')
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

如果是要讀取 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.
Enter fullscreen mode Exit fullscreen mode

存入 cookie 的範例:

@app.route('/')
def index():
    resp = make_response(render_template(...))
    resp.set_cookie('username', 'the username')
    return resp
Enter fullscreen mode Exit fullscreen mode

重導頁面與錯誤頁面

要重導使用 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()
Enter fullscreen mode Exit fullscreen mode

如果不想使用 Flask 預設的陽春錯誤頁,則利用 errorhandler() 修飾子來做客製:

from flask import render_template

@app.errorhandler(404)
def page_not_found(error):
    return render_template('page_not_found.html'), 404
Enter fullscreen mode Exit fullscreen mode

注意到 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
Enter fullscreen mode Exit fullscreen mode

拿 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
Enter fullscreen mode Exit fullscreen mode

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'))
Enter fullscreen mode Exit fullscreen mode

前面模板的章節有說過,模板引擎會幫我們把表單的 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')
Enter fullscreen mode Exit fullscreen mode

Top comments (0)

Timeless DEV post...

How to write a kickass README

Arguably the single most important piece of documentation for any open source project is the README. A good README not only informs people what the project does and who it is for but also how they use and contribute to it.

If you write a README without sufficient explanation of what your project does or how people can use it then it pretty much defeats the purpose of being open source as other developers are less likely to engage with or contribute towards it.