Flask 教程 第三章:Web表單

百度云搜索,搜各種資料:http://bdy.lqkweb.com

搜網(wǎng)盤,搜各種資料:http://www.swpan.cn

本文翻譯自 The Flask Mega-Tutorial Part III: Web Forms

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

第二章中我為應(yīng)用主頁創(chuàng)建了一個(gè)簡單的模板,并使用諸如用戶和用戶動(dòng)態(tài)的模擬對象。在本章中,我將解決這個(gè)應(yīng)用程序中仍然存在的眾多遺漏之一,那就是如何通過Web表單接受用戶的輸入。

Web表單是所有Web應(yīng)用程序中最基本的組成部分之一。 我將使用表單來為用戶發(fā)表動(dòng)態(tài)和登錄認(rèn)證提供途徑。

在繼續(xù)閱讀本章之前,確保你的microblog應(yīng)用程序狀態(tài)和上一章完結(jié)時(shí)一致,并且運(yùn)行時(shí)不會(huì)報(bào)任何錯(cuò)誤。

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

Flask-WTF簡介

我將使用Flask-WTF插件來處理本應(yīng)用中的Web表單,它對WTForms進(jìn)行了淺層次的封裝以便和Flask完美結(jié)合。這是本應(yīng)用引入的第一個(gè)Flask插件,但絕不是最后一個(gè)。插件是Flask生態(tài)中的舉足輕重的一部分,F(xiàn)lask故意設(shè)計(jì)為只包含核心功能以保持代碼的整潔,并暴露接口以對接解決不同問題的插件。

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

(venv) $ pip install flask-wtf

配置

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

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

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

上面的代碼雖然可以為應(yīng)用創(chuàng)建配置,但是我有松耦合的癖好。因此,我不會(huì)讓配置和應(yīng)用代碼處于同一個(gè)部分,而是使用稍微復(fù)雜點(diǎn)的結(jié)構(gòu),將配置保存到一個(gè)單獨(dú)的文件中。

使用類來存儲配置變量,才是我真正的風(fēng)格。我會(huì)將這個(gè)配置類存儲到單獨(dú)的Python模塊,以保持良好的組織結(jié)構(gòu)。下面就讓你見識一下這個(gè)存儲在頂級目錄下(microblog/config.py),名為config.py的模塊的配置類吧:

import os

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

簡單的不像話,有沒有? 配置設(shè)置被定義為Config類中的屬性。 一旦應(yīng)用程序需要更多配置選項(xiàng),直接依樣畫葫蘆,附加到這個(gè)類上即可,稍后如果我發(fā)現(xiàn)需要多個(gè)配置集,則可以創(chuàng)建它的子類?,F(xiàn)在則不用操心。

SECRET_KEY是我添加的唯一配置選項(xiàng),對大多數(shù)Flask應(yīng)用來說,它都是極其重要的。Flask及其一些擴(kuò)展使用密鑰的值作為加密密鑰,用于生成簽名或令牌。Flask-WTF插件使用它來保護(hù)網(wǎng)頁表單免受名為Cross-Site Request Forgery或CSRF(發(fā)音為“seasurf”)的惡意攻擊。顧名思義,密鑰應(yīng)該是隱密的,因?yàn)橛伤a(chǎn)生的令牌和簽名的加密強(qiáng)度保證,取決于除了可信維護(hù)者之外,沒有任何人能夠獲得它。

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

擁有了這樣一份配置文件,我還需要通知Flask讀取并使用它??梢栽谏蒄lask應(yīng)用之后,利用app.config.from_object()方法來完成這個(gè)操作:

from flask import Flask
from config import Config

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

from app import routes

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

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

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

用戶登錄表單

Flask-WTF插件使用Python類來表示W(wǎng)eb表單。表單類只需將表單的字段定義為類屬性即可。

為了再次踐行我的松耦合原則,我會(huì)將表單類單獨(dú)存儲到名為app/forms.py的模塊中。就讓我們來定義用戶登錄表單來做一個(gè)開始吧,它會(huì)要求用戶輸入username和password,并提供一個(gè)“remember me”的復(fù)選框和提交按鈕:

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

大多數(shù)Flask插件使用flask_ <name>命名約定來導(dǎo)入,F(xiàn)lask-WTF的所有內(nèi)容都在flask_wtf包中。在本例中,app/forms.py模塊的頂部從flask_wtf導(dǎo)入了名為FlaskForm的基類。

由于Flask-WTF插件本身不提供字段類型,因此我直接從WTForms包中導(dǎo)入了四個(gè)表示表單字段的類。每個(gè)字段類都接受一個(gè)描述或別名作為第一個(gè)參數(shù),并生成一個(gè)實(shí)例來作為LoginForm的類屬性。

你在一些字段中看到的可選參數(shù)validators用于驗(yàn)證輸入字段是否符合預(yù)期。DataRequired驗(yàn)證器僅驗(yàn)證字段輸入是否為空。更多的驗(yàn)證器將會(huì)在未來的表單中接觸到。

表單模板

下一步是將表單添加到HTML模板以便渲染到網(wǎng)頁上。 令人高興的是在LoginForm類中定義的字段支持自渲染為HTML元素,所以這個(gè)任務(wù)相當(dāng)簡單。 我將把登錄模板存儲在文件*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 %}

一如第二章,在這個(gè)模板中我再次使用了extends來繼承base.html基礎(chǔ)模板。事實(shí)上,我將會(huì)對所有的模板繼承基礎(chǔ)模板,以保持頂部導(dǎo)航欄風(fēng)格統(tǒng)一。

這個(gè)模板需要一個(gè)form參數(shù)的傳入到渲染模板的函數(shù)中,form來自于LoginForm類的實(shí)例化,不過我現(xiàn)在還沒有編寫它。

HTML<form>元素被用作Web表單的容器。 表單的action屬性告訴瀏覽器在提交用戶在表單中輸入的信息時(shí)應(yīng)該請求的URL。 當(dāng)action設(shè)置為空字符串時(shí),表單將被提交給當(dāng)前地址欄中的URL,即當(dāng)前頁面。 method屬性指定了將表單提交給服務(wù)器時(shí)應(yīng)該使用的HTTP請求方法。 默認(rèn)情況下是用GET請求發(fā)送,但幾乎在所有情況下,使用POST請求會(huì)提供更好的用戶體驗(yàn),因?yàn)檫@種類型的請求可以在請求的主體中提交表單數(shù)據(jù), GET請求將表單字段添加到URL,會(huì)使瀏覽器地址欄變得混亂。

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

如果你以前編寫過HTML Web表單,那么你會(huì)發(fā)現(xiàn)一個(gè)奇怪的現(xiàn)象——在此模板中沒有HTML表單元素,這是因?yàn)楸韱蔚淖侄螌ο蟮脑阡秩緯r(shí)會(huì)自動(dòng)轉(zhuǎn)化為HTML元素。 我只需在需要字段標(biāo)簽的地方加上{{ form.<field_name>.label }},需要這個(gè)字段的地方加上{{ form.<field_name>() }}。 對于需要附加HTML屬性的字段,可以作為關(guān)鍵字參數(shù)傳遞到函數(shù)中。 此模板中的username和password字段將size作為參數(shù),將其作為屬性添加到<input> HTML元素中。 你也可以通過這種手段為表單字段設(shè)置class和id屬性。

表單視圖

完成這個(gè)表單的最后一步就是編寫一個(gè)新的視圖函數(shù)來渲染上面創(chuàng)建的模板。

函數(shù)的邏輯只需創(chuàng)建一個(gè)form實(shí)例,并將其傳入渲染模板的函數(shù)中即可,然后用/login URL來關(guān)聯(lián)它。這個(gè)視圖函數(shù)也存儲到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導(dǎo)入LoginForm類,并生成了一個(gè)實(shí)例傳入模板。form=form的語法看起來奇怪,這是Python函數(shù)或方法傳入關(guān)鍵字參數(shù)的方式,左邊的form代表在模板中引用的變量名稱,右邊則是傳入的form實(shí)例。這就是獲取表單字段渲染結(jié)果的所有代碼了。

在基礎(chǔ)模板templates/base.html的導(dǎo)航欄上添加登錄的鏈接,以便訪問:

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

此時(shí),你可以驗(yàn)證結(jié)果了。運(yùn)行該應(yīng)用,在瀏覽器的地址欄中輸入http://localhost:5000/,然后點(diǎn)擊頂部導(dǎo)航欄中的“Login”鏈接來查看新的登錄表單。 是不是非常炫酷?

登錄表單

接收表單數(shù)據(jù)

點(diǎn)擊提交按鈕,瀏覽器將顯示“Method Not Allowed”錯(cuò)誤。為什么呢? 這是因?yàn)橹暗牡卿浺晥D功能到目前為止只完成了一半的工作。 它可以在網(wǎng)頁上顯示表單,但沒有邏輯來處理用戶提交的數(shù)據(jù)。Flask-WTF可以輕松完成這部分工作, 以下是視圖函數(shù)的更新版本,它接受和驗(yàn)證用戶提交的數(shù)據(jù):

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)

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

form.validate_on_submit()實(shí)例方法會(huì)執(zhí)行form校驗(yàn)的工作。當(dāng)瀏覽器發(fā)起GET請求的時(shí)候,它返回False,這樣視圖函數(shù)就會(huì)跳過if塊中的代碼,直接轉(zhuǎn)到視圖函數(shù)的最后一句來渲染模板。

當(dāng)用戶在瀏覽器點(diǎn)擊提交按鈕后,瀏覽器會(huì)發(fā)送POST請求。form.validate_on_submit()就會(huì)獲取到所有的數(shù)據(jù),運(yùn)行字段各自的驗(yàn)證器,全部通過之后就會(huì)返回True,這表示數(shù)據(jù)有效。不過,一旦有任意一個(gè)字段未通過驗(yàn)證,這個(gè)實(shí)例方法就會(huì)返回False,引發(fā)類似GET請求那樣的表單的渲染并返回給用戶。稍后我會(huì)在添加代碼以實(shí)現(xiàn)在驗(yàn)證失敗的時(shí)候顯示一條錯(cuò)誤消息。

當(dāng)form.validate_on_submit()返回True時(shí),登錄視圖函數(shù)調(diào)用從Flask導(dǎo)入的兩個(gè)新函數(shù)。 flash()函數(shù)是向用戶顯示消息的有效途徑。 許多應(yīng)用使用這個(gè)技術(shù)來讓用戶知道某個(gè)動(dòng)作是否成功。我將使用這種機(jī)制作為臨時(shí)解決方案,因?yàn)槲覜]有基礎(chǔ)架構(gòu)來真正地登錄用戶。 顯示一條消息來確認(rèn)應(yīng)用已經(jīng)收到登錄認(rèn)證憑據(jù),我認(rèn)為對當(dāng)前來說已經(jīng)足夠了。

登錄視圖函數(shù)中使用的第二個(gè)新函數(shù)是redirect()。這個(gè)函數(shù)指引瀏覽器自動(dòng)重定向到它的參數(shù)所關(guān)聯(lián)的URL。當(dāng)前視圖函數(shù)使用它將用戶重定向到應(yīng)用的主頁。

當(dāng)你調(diào)用flash()函數(shù)后,F(xiàn)lask會(huì)存儲這個(gè)消息,但是卻不會(huì)奇跡般地直接出現(xiàn)在頁面上。模板需要將消息渲染到基礎(chǔ)模板中,才能讓所有派生出來的模板都能顯示出來。更新后的基礎(chǔ)模板代碼如下:

<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結(jié)構(gòu)在當(dāng)前模板的上下文中來將get_flashed_messages()的結(jié)果賦值給變量messagesget_flashed_messages()是Flask中的一個(gè)函數(shù),它返回用flash()注冊過的消息列表。接下來的條件結(jié)構(gòu)用來檢查變量messages是否包含元素,如果有,則在<ul>元素中,為每條消息用<li>元素來包裹渲染。這種渲染的樣式結(jié)果看起來不會(huì)美觀,之后會(huì)有主題講到Web應(yīng)用的樣式。

閃現(xiàn)消息的一個(gè)有趣的屬性是,一旦通過get_flashed_messages函數(shù)請求了一次,它們就會(huì)從消息列表中移除,所以在調(diào)用flash()函數(shù)后它們只會(huì)出現(xiàn)一次。

時(shí)機(jī)成熟,再次測試表單吧,將username和password字段留空并點(diǎn)擊提交按鈕來觀察DataRequired驗(yàn)證器是如何中斷提交處理流程的。

完善字段驗(yàn)證

表單字段的驗(yàn)證器可防止無效數(shù)據(jù)被接收到應(yīng)用中。 應(yīng)用處理無效表單輸入的方式是重新顯示表單,以便用戶進(jìn)行更正。

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

實(shí)際上,表單驗(yàn)證器已經(jīng)生成了這些描述性錯(cuò)誤消息,所缺少的不過是模板中的一些額外的邏輯來渲染它們。

這是給username和password字段添加了驗(yàn)證描述性錯(cuò)誤消息渲染邏輯之后的登錄模板:

{% 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循環(huán)以便用紅色字體來渲染驗(yàn)證器添加的錯(cuò)誤信息。通常情況下,擁有驗(yàn)證器的字段都會(huì)用form.<field_name>.errors來渲染錯(cuò)誤信息。 一個(gè)字段的驗(yàn)證錯(cuò)誤信息結(jié)果是一個(gè)列表,因?yàn)樽侄慰梢愿郊佣鄠€(gè)驗(yàn)證器,并且多個(gè)驗(yàn)證器都可能會(huì)提供錯(cuò)誤消息以顯示給用戶。

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

表單驗(yàn)證

生成鏈接

現(xiàn)在的登錄表單已經(jīng)相當(dāng)完整了,但在結(jié)束本章之前,我想討論在模板和重定向中包含鏈接的妥當(dāng)方法。 到目前為止,你已經(jīng)看到了一些定義鏈接的例子。 例如,這是當(dāng)前基礎(chǔ)模板中的導(dǎo)航欄代碼:

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

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

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

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

為了更好地管理這些鏈接,F(xiàn)lask提供了一個(gè)名為url_for()的函數(shù),它使用URL到視圖函數(shù)的內(nèi)部映射關(guān)系來生成URL。 例如,url_for('login')返回/loginurl_for('index')返回/index。 url_for()的參數(shù)是endpoint名稱,也就是視圖函數(shù)的名字。

你可能會(huì)問,為什么使用函數(shù)名稱而不是URL? 事實(shí)是,URL比起視圖函數(shù)名稱變更的可能性更高。 稍后你會(huì)了解到的第二個(gè)原因是,一些URL中包含動(dòng)態(tài)組件,手動(dòng)生成這些URL需要連接多個(gè)元素,枯燥乏味且容易出錯(cuò)。 url_for()生成這種復(fù)雜的URL就方便許多。

因此,從現(xiàn)在起,一旦我需要生成應(yīng)用鏈接,我就會(huì)使用url_for()?;A(chǔ)模板中的導(dǎo)航欄部分代碼變更如下:

    <div>
        Microblog:
        <a href="{{ url_for('index') }}">Home</a>
        <a href="{{ url_for('login') }}">Login</a>
    </div>

login()視圖函數(shù)也做了相應(yīng)變更:

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'))
    # ...
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容