Flask 快速指南
Tags: python, flask
Published: 2021-10-23 06:44:28

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


## 安裝

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

安裝 Flask 套件：

```sh
> pipenv install Flask
```

## Hello, World!

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

```Python
from flask import Flask
app = Flask(__name__)

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

注意大小寫別打錯了！

切換到終端機，先加個環境變數讓 `flask run` 的時候知道要去跑這支腳本：

```sh
(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 有提示網址以及 <kbd>CTRL+C</kbd> 可以退出的字樣。瀏覽器打開 127.0.0.1:5000 應該就可以看到那親切的 Hello World。

## 除錯模式

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

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

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

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

```sh
(flask-app) > export FLASK_ENV=development
(flask-app) > flask run
```

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

## 路由

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

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

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

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

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

開始來點動態路由，動態路由的範例：

```Python
@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

路由字串的最後，斜線或沒斜線在行為上是有所差異的：

```Python
@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，別拼錯了）：


```Python
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：

```Python
url_for('static', filename='style.css')
```

上面的例子會對應到 static/style.css。

## 模板

在此之前， 路由、controller、view 都摻在一起寫，從這裡開始要引進模板的概念，Flask 使用 Jinja2 做為模板引擎，有了模板引擎，就可以在 view 內寫 view 自己的邏輯，並且幫你把變數內的 HTML 標籤過濾掉，還有就是可以達成組版的效果。

使用 `render_template()` 方法來使用模板：

```Python
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 為例的模板內容：

```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` 屬性內。示例：

```Python
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()` 方法：

```Python
searchword = request.args.get('key', '')
```

如果是要取用客戶端上傳的檔案，先確定在前端 HTML 表單的設定正確的屬性 `enctype="multipart/form-data"`，瀏覽器才會正確的把檔案上傳。調用 `request` 的 `files` 屬性就可以調用到檔案：


```Python
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()` 方法過濾掉：

```Python
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 的範例：

```Python
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 的範例：

```Python
@app.route('/')
def index():
    resp = make_response(render_template(...))
    resp.set_cookie('username', 'the username')
    return resp
```

## 重導頁面與錯誤頁面

要重導使用 `redirect()` 方法，要中斷並報錯用 `abort()` 方法：

```Python
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()` 修飾子來做客製：

```Python
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 如下：

```Python
@app.errorhandler(404)
def not_found(error):
    return render_template('error.html'), 404
```

拿 `make_response()` 來幫它加工加上一組 header 資訊：

```Python
@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：

```Python
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` 模組，可以簡單調用：

```Python
app.logger.debug('A value for debugging')
app.logger.warning('A warning occurred (%d apples)', 42)
app.logger.error('An error occurred')
```