flask學習:Web表單

這是Flask Mega-Tutorial系列的第三部分,我將告訴你如何使用Web表單。

在第二章中我爲應用主頁創建了一個簡單的模板,並使用諸如用戶和用戶動態的模擬對象。在本章中,我將解決這個應用程序中仍然存在的衆多遺漏之一,那就是如何通過Web表單接受用戶的輸入。

Web表單是所有Web應用程序中最基本的組成部分之一。 我將使用表單來爲用戶發表動態和登錄認證提供途徑。

在繼續閱讀本章之前,確保你的microblog應用程序狀態和上一章完結時一致,並且運行時不會報任何錯誤。

本章的GitHub鏈接爲:Browse, Zip, Diff.

Flask-WTF簡介
我將使用Flask-WTF插件來處理本應用中的Web表單,它對WTForms進行了淺層次的封裝以便和Flask完美結合。這是本應用引入的第一個Flask插件,但絕不是最後一個。插件是Flask生態中的舉足輕重的一部分,Flask故意設計爲只包含核心功能以保持代碼的整潔,並暴露接口以對接解決不同問題的插件。

Flask插件都是常規的Python三方包,可以使用pip安裝。 那就繼續在你的虛擬環境中安裝Flask-WTF吧:

(venv) $ pip install flask-wtf

配置

到目前爲止,這個應用程序都非常簡單,因此我不需要考慮它的配置。 但是,除了最簡單的應用,你會發現Flask(也可能是Flask插件)爲使用者提供了一些可自由配置的選項。你需要決定傳入什麼樣的配置變量列表到框架中。

有幾種途徑來爲應用指定配置選項。最基本的解決方案是使用app.config對象,它是一個類似字典的對象,可以將配置以鍵值的方式存儲其中。例如,你可以這樣做:

app = Flask(__name__)
app.config['SECRET_KEY'] = 'you-will-never-guess'
# ... add more variables here as needed

上面的代碼雖然可以爲應用創建配置,但是我有鬆耦合的癖好。因此,我不會讓配置和應用代碼處於同一個部分,而是使用稍微複雜點的結構,將配置保存到一個單獨的文件中。

使用類來存儲配置變量,纔是我真正的風格。我會將這個配置類存儲到單獨的Python模塊,以保持良好的組織結構。下面就讓你見識一下這個存儲在頂級目錄下(microblog/config.py),名爲config.py的模塊的配置類吧:

import os

class Config(object):
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'

簡單的不像話,有沒有? 配置設置被定義爲Config類中的屬性。 一旦應用程序需要更多配置選項,直接依樣畫葫蘆,附加到這個類上即可,稍後如果我發現需要多個配置集,則可以創建它的子類。現在則不用操心。

SECRET_KEY是我添加的唯一配置選項,對大多數Flask應用來說,它都是極其重要的。Flask及其一些擴展使用密鑰的值作爲加密密鑰,用於生成簽名或令牌。Flask-WTF插件使用它來保護網頁表單免受名爲Cross-Site Request Forgery或CSRF(發音爲“seasurf”)的惡意攻擊。顧名思義,密鑰應該是隱密的,因爲由它產生的令牌和簽名的加密強度保證,取決於除了可信維護者之外,沒有任何人能夠獲得它。

密鑰被定義成由or運算符連接兩個項的表達式。第一個項查找環境變量SECRET_KEY的值,第二個項是一個硬編碼的字符串。這種首先檢查環境變量中是否存在這個配置,找不到的情況下就使用硬編碼字符串的配置變量的模式你將會反覆看到。在開發階段,安全性要求較低,因此可以直接使用硬編碼字符串。但是,當應用部署到生產服務器上的時候,我將設置一個獨一無二且難以揣摩的環境變量,這樣,服務器就擁有了一個別人未知的安全密鑰了。

擁有了這樣一份配置文件,我還需要通知Flask讀取並使用它。可以在生成Flask應用之後,利用app.config.from_object()方法來完成這個操作:

from flask import Flask
from config import Config

app = Flask(__name__)
app.config.from_object(Config)

from app import routes

導入Config類的方式,乍一看可能會讓人感到困惑,不過如果你注意到從flask包導入Flask類的過程,就會發現這其實是類似的操作。 顯而易見,小寫的“config”是Python模塊config.py的名字,另一個含有大寫“C”的是類。

正如我上面提到的,可以使用app.config中的字典語法來訪問配置項。 在下面的Python交互式會話中,你可以看到密鑰的值:

>>> from microblog import app
>>> app.config['SECRET_KEY']
'you-will-never-guess'

用戶登錄表單
Flask-WTF插件使用Python類來表示Web表單。表單類只需將表單的字段定義爲類屬性即可。

爲了再次踐行我的鬆耦合原則,我會將表單類單獨存儲到名爲app/forms.py的模塊中。就讓我們來定義用戶登錄表單來做一個開始吧,它會要求用戶輸入username和password,並提供一個“remember me”的複選框和提交按鈕:

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired

class LoginForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    password = PasswordField('Password', validators=[DataRequired()])
    remember_me = BooleanField('Remember Me')
    submit = SubmitField('Sign In')

大多數Flask插件使用flask_ 命名約定來導入,Flask-WTF的所有內容都在flask_wtf包中。在本例中,app/forms.py模塊的頂部從flask_wtf導入了名爲FlaskForm的基類。

由於Flask-WTF插件本身不提供字段類型,因此我直接從WTForms包中導入了四個表示表單字段的類。每個字段類都接受一個描述或別名作爲第一個參數,並生成一個實例來作爲LoginForm的類屬性。

你在一些字段中看到的可選參數validators用於驗證輸入字段是否符合預期。DataRequired驗證器僅驗證字段輸入是否爲空。更多的驗證器將會在未來的表單中接觸到。

表單模板
下一步是將表單添加到HTML模板以便渲染到網頁上。 令人高興的是在LoginForm類中定義的字段支持自渲染爲HTML元素,所以這個任務相當簡單。 我將把登錄模板存儲在文件*app/templates/login.html *中,代碼如下:

{% extends "base.html" %}

{% block content %}
    <h1>Sign In</h1>
    <form action="" method="post" novalidate>
        {{ form.hidden_tag() }}
        <p>
            {{ form.username.label }}<br>
            {{ form.username(size=32) }}
        </p>
        <p>
            {{ form.password.label }}<br>
            {{ form.password(size=32) }}
        </p>
        <p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
        <p>{{ form.submit() }}</p>
    </form>
{% endblock %}

一如第二章,在這個模板中我再次使用了extends來繼承base.html基礎模板。事實上,我將會對所有的模板繼承基礎模板,以保持頂部導航欄風格統一。

這個模板需要一個form參數的傳入到渲染模板的函數中,form來自於LoginForm類的實例化,不過我現在還沒有編寫它。

HTML元素被用作Web表單的容器。 表單的action屬性告訴瀏覽器在提交用戶在表單中輸入的信息時應該請求的URL。 當action設置爲空字符串時,表單將被提交給當前地址欄中的URL,即當前頁面。 method屬性指定了將表單提交給服務器時應該使用的HTTP請求方法。 默認情況下是用GET請求發送,但幾乎在所有情況下,使用POST請求會提供更好的用戶體驗,因爲這種類型的請求可以在請求的主體中提交表單數據, GET請求將表單字段添加到URL,會使瀏覽器地址欄變得混亂。

form.hidden_tag()模板參數生成了一個隱藏字段,其中包含一個用於保護表單免受CSRF攻擊的token。 對於保護表單,你需要做的所有事情就是在模板中包括這個隱藏的字段,並在Flask配置中定義SECRET_KEY變量,Flask-WTF會完成剩下的工作。

如果你以前編寫過HTML Web表單,那麼你會發現一個奇怪的現象——在此模板中沒有HTML表單元素,這是因爲表單的字段對象的在渲染時會自動轉化爲HTML元素。 我只需在需要字段標籤的地方加上{{ form.<field_name>.label }},需要這個字段的地方加上{{ form.<field_name>() }}。 對於需要附加HTML屬性的字段,可以作爲關鍵字參數傳遞到函數中。 此模板中的username和password字段將size作爲參數,將其作爲屬性添加到 HTML元素中。 你也可以通過這種手段爲表單字段設置class和id屬性。

表單視圖
完成這個表單的最後一步就是編寫一個新的視圖函數來渲染上面創建的模板。

函數的邏輯只需創建一個form實例,並將其傳入渲染模板的函數中即可,然後用/login URL來關聯它。這個視圖函數也存儲到app/routes.py模塊中,代碼如下:

from flask import render_template
from app import app
from app.forms import LoginForm

# ...

@app.route('/login')
def login():
    form = LoginForm()
    return render_template('login.html', title='Sign In', form=form)

我從forms.py導入LoginForm類,並生成了一個實例傳入模板。form=form的語法看起來奇怪,這是Python函數或方法傳入關鍵字參數的方式,左邊的form代表在模板中引用的變量名稱,右邊則是傳入的form實例。這就是獲取表單字段渲染結果的所有代碼了。

在基礎模板templates/base.html的導航欄上添加登錄的鏈接,以便訪問:

<div>
    Microblog:
    <a href="/index">Home</a>
    <a href="/login">Login</a>
</div>

此時,你可以驗證結果了。運行該應用,在瀏覽器的地址欄中輸入http://localhost:5000/,然後點擊頂部導航欄中的“Login”鏈接來查看新的登錄表單。 是不是非常炫酷?

接收表單數據
點擊提交按鈕,瀏覽器將顯示“Method Not Allowed”錯誤。爲什麼呢? 這是因爲之前的登錄視圖功能到目前爲止只完成了一半的工作。 它可以在網頁上顯示錶單,但沒有邏輯來處理用戶提交的數據。Flask-WTF可以輕鬆完成這部分工作, 以下是視圖函數的更新版本,它接受和驗證用戶提交的數據:

from flask import render_template, flash, redirect

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        flash('Login requested for user {}, remember_me={}'.format(
            form.username.data, form.remember_me.data))
        return redirect('/index')
    return render_template('login.html', title='Sign In', form=form)

這個版本中的第一個新東西是路由裝飾器中的methods參數。 它告訴Flask這個視圖函數接受GET和POST請求,並覆蓋了默認的GET。 HTTP協議規定對GET請求需要返回信息給客戶端(本例中是瀏覽器)。 本應用的所有GET請求都是如此。 當瀏覽器向服務器提交表單數據時,通常會使用POST請求(實際上用GET請求也可以,但這不是推薦的做法)。之前的“Method Not Allowed”錯誤正是由於視圖函數還未配置允許POST請求。 通過傳入methods參數,你就能告訴Flask哪些請求方法可以被接受。

form.validate_on_submit()實例方法會執行form校驗的工作。當瀏覽器發起GET請求的時候,它返回False,這樣視圖函數就會跳過if塊中的代碼,直接轉到視圖函數的最後一句來渲染模板。

當用戶在瀏覽器點擊提交按鈕後,瀏覽器會發送POST請求。form.validate_on_submit()就會獲取到所有的數據,運行字段各自的驗證器,全部通過之後就會返回True,這表示數據有效。不過,一旦有任意一個字段未通過驗證,這個實例方法就會返回False,引發類似GET請求那樣的表單的渲染並返回給用戶。稍後我會在添加代碼以實現在驗證失敗的時候顯示一條錯誤消息。

當form.validate_on_submit()返回True時,登錄視圖函數調用從Flask導入的兩個新函數。 flash()函數是向用戶顯示消息的有效途徑。 許多應用使用這個技術來讓用戶知道某個動作是否成功。我將使用這種機制作爲臨時解決方案,因爲我沒有基礎架構來真正地登錄用戶。 顯示一條消息來確認應用已經收到登錄認證憑據,我認爲對當前來說已經足夠了。

登錄視圖函數中使用的第二個新函數是redirect()。這個函數指引瀏覽器自動重定向到它的參數所關聯的URL。當前視圖函數使用它將用戶重定向到應用的主頁。

當你調用flash()函數後,Flask會存儲這個消息,但是卻不會奇蹟般地直接出現在頁面上。模板需要將消息渲染到基礎模板中,才能讓所有派生出來的模板都能顯示出來。更新後的基礎模板代碼如下:

<html>
    <head>
        {% if title %}
        <title>{{ title }} - microblog</title>
        {% else %}
        <title>microblog</title>
        {% endif %}
    </head>
    <body>
        <div>
            Microblog:
            <a href="/index">Home</a>
            <a href="/login">Login</a>
        </div>
        <hr>
        {% with messages = get_flashed_messages() %}
        {% if messages %}
        <ul>
            {% for message in messages %}
            <li>{{ message }}</li>
            {% endfor %}
        </ul>
        {% endif %}
        {% endwith %}
        {% block content %}{% endblock %}
    </body>
</html>

此處我用了with結構在當前模板的上下文中來將get_flashed_messages()的結果賦值給變量messages。get_flashed_messages()是Flask中的一個函數,它返回用flash()註冊過的消息列表。接下來的條件結構用來檢查變量messages是否包含元素,如果有,則在

    元素中,爲每條消息用
  • 元素來包裹渲染。這種渲染的樣式結果看起來不會美觀,之後會有主題講到Web應用的樣式。

閃現消息的一個有趣的屬性是,一旦通過get_flashed_messages函數請求了一次,它們就會從消息列表中移除,所以在調用flash()函數後它們只會出現一次。

時機成熟,再次測試表單吧,將username和password字段留空並點擊提交按鈕來觀察DataRequired驗證器是如何中斷提交處理流程的。

完善字段驗證

表單字段的驗證器可防止無效數據被接收到應用中。 應用處理無效表單輸入的方式是重新顯示錶單,以便用戶進行更正。

如果你嘗試過提交無效的數據,相信你會注意到,雖然驗證機制查無遺漏,卻沒有給出表單錯誤的具體線索。下一個任務是通過在驗證失敗的每個字段旁邊添加有意義的錯誤消息來改善用戶體驗。

實際上,表單驗證器已經生成了這些描述性錯誤消息,所缺少的不過是模板中的一些額外的邏輯來渲染它們。

這是給username和password字段添加了驗證描述性錯誤消息渲染邏輯之後的登錄模板:

{% extends "base.html" %}

{% block content %}
    <h1>Sign In</h1>
    <form action="" method="post" novalidate>
        {{ form.hidden_tag() }}
        <p>
            {{ form.username.label }}<br>
            {{ form.username(size=32) }}<br>
            {% for error in form.username.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.password.label }}<br>
            {{ form.password(size=32) }}<br>
            {% for error in form.password.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
        <p>{{ form.submit() }}</p>
    </form>
{% endblock %}

我做的唯一的改變是,在username和password字段之後添加for循環以便用紅色字體來渲染驗證器添加的錯誤信息。通常情況下,擁有驗證器的字段都會用form.<field_name>.errors來渲染錯誤信息。 一個字段的驗證錯誤信息結果是一個列表,因爲字段可以附加多個驗證器,並且多個驗證器都可能會提供錯誤消息以顯示給用戶。

如果你嘗試在未填寫username和password字段的情況下提交表單,就可以看到顯眼的紅色錯誤信息了。

生成鏈接
現在的登錄表單已經相當完整了,但在結束本章之前,我想討論在模板和重定向中包含鏈接的妥當方法。 到目前爲止,你已經看到了一些定義鏈接的例子。 例如,這是當前基礎模板中的導航欄代碼:

Microblog: Home Login

登錄視圖函數同樣定義了一個傳入到redirect()函數作爲參數的鏈接:

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        # ...
        return redirect('/index')
    # ...

直接在模板和源文件中硬編碼鏈接存在隱患,如果有一天你決定重新組織鏈接,那麼你將不得不在整個應用中搜索並替換這些鏈接。

爲了更好地管理這些鏈接,Flask提供了一個名爲url_for()的函數,它使用URL到視圖函數的內部映射關係來生成URL。 例如,url_for(‘login’)返回/login,url_for(‘index’)返回/index。 url_for()的參數是endpoint名稱,也就是視圖函數的名字。

你可能會問,爲什麼使用函數名稱而不是URL? 事實是,URL比起視圖函數名稱變更的可能性更高。 稍後你會瞭解到的第二個原因是,一些URL中包含動態組件,手動生成這些URL需要連接多個元素,枯燥乏味且容易出錯。 url_for()生成這種複雜的URL就方便許多。

因此,從現在起,一旦我需要生成應用鏈接,我就會使用url_for()。基礎模板中的導航欄部分代碼變更如下:

Microblog: Home Login

login()視圖函數也做了相應變更:

from flask import render_template, flash, redirect, url_for

# ...

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        # ...
        return redirect(url_for('index'))
    # ...

在這裏插入圖片描述

發佈了22 篇原創文章 · 獲贊 11 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章