The Flask Mega_Tutorial Part3 Web表單

這是 Flask Mega-Tutorial 系列的第三部分,本節(jié)我們將學(xué)習(xí)如何使用Web表單。
第二章節(jié) ,我們?yōu)槌绦虻氖醉搫?chuàng)建了一個簡單的模板,并使用虛假數(shù)據(jù)為目前還不存在的部分如用戶帖子等占位。在本章,我們將修補(bǔ)目前程序里的諸多漏洞之一,尤其是如何通過表單接受用戶輸入。

任何web應(yīng)用當(dāng)中Web 表單都是基本構(gòu)件之一,我們將使用表單來允許用戶發(fā)帖以及登錄進(jìn)入應(yīng)用程序。

在開始之前,請確認(rèn)你已經(jīng)完成前幾章的 microblog 程序,并且能夠無錯運(yùn)行起來。

Flask-WTF簡介

為了處理Web表單,我將使用 Flask-WTF 擴(kuò)展,這是對 WTForms 輕量化封裝,很好的跟Flask集成在了一起。這是我要給你推薦的第一個擴(kuò)展,但不會是最后一個。 擴(kuò)展(Extensions)是Flask生態(tài)系統(tǒng)非常重要的一部分,他們提供了Flask有意忽略掉的問題的解決方案。

Flask 擴(kuò)展是常規(guī)的Python包,使用pip安裝。你可以先去你的虛擬環(huán)境里去安裝Flask-WTF:

(venv) $ pip install flask-wtf

配置

到目前為止程序還是非常簡單的,因此我不需要操心它的配置configuration)。但,你會發(fā)現(xiàn),除了這些最簡單的情況之外,F(xiàn)lask(也包括其擴(kuò)展)為自身運(yùn)作提供了相當(dāng)?shù)淖杂啥?,你需要做一些決定,通過配置變量列表傳遞給框架。
關(guān)于特定的配置選項(xiàng),有幾種格式。最基本的方法就是在 app.config文件中定義變量為鍵值,用字典樣式處理變量。舉例來說,你可以這樣做:

app = Flask(__name__)
app.config['SECRET_KEY'] = 'you-will-never-guess'
# ... 在此添加更多的變量鍵值

雖然上述語法足以為Flask創(chuàng)建配置項(xiàng),但我更傾向于強(qiáng)制執(zhí)行 關(guān)注點(diǎn)分離 原則, 我喜歡使用一個稍微復(fù)雜一點(diǎn)的結(jié)構(gòu)來把配置項(xiàng)分離放到一個獨(dú)立文件當(dāng)中,而不是把配置項(xiàng)塞到程序中的同一個位置上。

我喜歡的一個可擴(kuò)展格式就是,使用一個類來存儲配置變量。為了保證良好的組織性,我要在一個獨(dú)立的Python模塊中創(chuàng)建這個類。下面你要看到的就是本程序的新配置類,它被存儲在頂層文件夾的 config.py 模塊中。

config.py: 密鑰配置

import os

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

非常簡潔,對吧? 配置設(shè)定在Config類中被定義為類的變量。一旦程序需要更多的配置項(xiàng),那就可以繼續(xù)想這個類當(dāng)中添加即可。而將來,如果我發(fā)現(xiàn)我還需要更多的配置,我還可以創(chuàng)建這個配置類的子類進(jìn)行擴(kuò)展。當(dāng)然,現(xiàn)在我們還不需要操心這個。

目前我們添加的SECRET_KEY 配置變量在大多數(shù)Flask應(yīng)用程序中是非常重要的一部分。Flask和它的一些擴(kuò)展要使用這個密鑰值來保護(hù)Web表單避免跨站攻擊的威脅 Cross-Site Request Forgery (縮寫CSRF,發(fā)音為 "seasurf")。 顧名思義,密鑰應(yīng)該是保密的,因?yàn)橛盟傻牧钆坪秃灻膹?qiáng)度僅取決于受信任維護(hù)者,除此之外,任何人都不應(yīng)該知曉。

密鑰的值被設(shè)置為由or 操作符鏈接的兩項(xiàng)組成的表達(dá)式。第一項(xiàng)查找環(huán)境變量的值,也被稱為SECRET_KEY。第二項(xiàng),只是硬編碼(意思就是直接內(nèi)嵌在程序當(dāng)中的,固定常量)的字符串。 對于配置變量,你會發(fā)現(xiàn)我經(jīng)常重復(fù)使用這種模式。其思想是,如果在環(huán)境變量中定義了配置變量就直接使用,如果沒有那么就使用硬編碼字符串作為替代。當(dāng)你在開發(fā)程序時,安全要求是比較低的,所以你可以忽略這個設(shè)置直接使用硬編碼。但如果程序已經(jīng)配置運(yùn)行在生產(chǎn)服務(wù)器上了,我就會設(shè)置一個唯一的,很難猜的環(huán)境變量值,這樣服務(wù)器就有一個別人無從得知的安全密鑰(而且你可以隨時更換之,無需修改程序)。
現(xiàn)在,我們有一個配置文件,我需要告訴Flask讀取并應(yīng)用其中的配置。這應(yīng)該在Flask應(yīng)用實(shí)例創(chuàng)建之后,調(diào)入 app.config.from_object() 方法:

app/init.py: Flask 配置

from flask import Flask
from config import Config

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

from app import routes

一開始你可能不好理解這種導(dǎo)入Config類的方式,但如果你看看Flask類(大寫的F)是如何被從flask包(小寫f)中導(dǎo)入的,你會注意到我也是同樣導(dǎo)入配置的。 小寫的 "config" 是Python模塊config.py的名字,很明顯,大寫的“C”的才是實(shí)際的類。

如上所述,配置項(xiàng)目可以通過app.config以字典類型被訪問。下面你可以看到我通過Python交互器檢查密鑰值的一個會話:

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

用戶登錄

Flask-WTF 擴(kuò)展使用Python類形式來描述Web表單,這個表單類簡單的把表單的字段定義成類的變量。

再一次,基于關(guān)注點(diǎn)分離原則,我要使用一個新的模塊 app/forms.py 來存儲我的web表單類。首先,我們定一個用戶登錄表單,要求用戶輸入用戶名和密碼,當(dāng)然還包括“記住我”(remember me)和“提交”(submit)按鈕:
app/forms.py: 登錄表單

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可擴(kuò)展使用 flask_<name> 的方式作為頂級符號命名約定。本例中,F(xiàn)lask-WTF 的所有符號都在flask_wtf下。同時也在這里從app/forms.py頂部導(dǎo)入“FlaskForm”基類。

由于Flask-WTF不支持自定義版本,因此表單中我所用的四個描述字段類型的類直接從WTForms包中導(dǎo)入。每一個字段,都會在LoginForm類中創(chuàng)建一個變量對象。每個字段都會有個描述(descriptoin)或標(biāo)簽(label)作為第一個參數(shù)。
你看到在某些字段中會有參數(shù)選項(xiàng) validators,這將為字段添加有效性驗(yàn)證行為。 DataRequired 驗(yàn)證器只是簡單的檢查一下字段是否為空。還有很多可用的驗(yàn)證器,其中一些可以用在其他表單中

表單模板

以下步驟是給HTML模板添加表單,這樣就可以渲染到一個web頁面。好消息是在LoginForm類中定義的字段知道如何把自己渲染成HTML,因此任務(wù)非常簡單。下面就是登錄模板,保存于app/templates/login.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再一次復(fù)用base.html 模板。將來所有模板都會如此繼承,這樣一來確保整個應(yīng)用程序的頁面布局統(tǒng)一具備一個頂部導(dǎo)航欄。

這個模板將用到一個名為form的變量參數(shù),它是LoginForm類的實(shí)例化對象。這個參數(shù)將在login視圖函數(shù)中被發(fā)送,當(dāng)然現(xiàn)在還沒編寫:-D

HTML <form> 元素用作web表單的容器,表單的action 屬性用來告訴瀏覽器:當(dāng)提交用戶輸入的信息時應(yīng)該使用的URL(統(tǒng)一資源定位符)。如果該動作設(shè)置為空字符,表單就會被提交給當(dāng)前地址欄顯示的這個URL——也就是渲染出當(dāng)前頁面的這個URL。 method 屬性指定當(dāng)表單提交給服務(wù)器時要使用的HTTP請求方法

(HTTP請求方法深入請參考HTTP協(xié)議,常用的如:get獲取資源,post傳送主體,head獲取頭部,delete刪除,put傳輸文件)。

默認(rèn)發(fā)送采用 GET 請求,但大部分情況下,采用 POST 請求用戶體驗(yàn)會更好一些,因?yàn)樗驯韱螖?shù)據(jù)包含到了請求體內(nèi)部。而GET請求則是把表單數(shù)據(jù)統(tǒng)統(tǒng)附加到URL中去,這就會導(dǎo)致瀏覽器地址亂糟糟一大串。 novalidate 屬性被用來告訴瀏覽器 不執(zhí)行 表單中該字段的 有效性驗(yàn)證, 其作用就是把該工作留給運(yùn)行在服務(wù)器上的Flask程序去做。使用 novalidate 完全是可選的,但對于這個表單來說這么設(shè)置是非常必要的,因?yàn)橹挥性O(shè)置該項(xiàng)稍后你才能去測試服務(wù)器端的有效性驗(yàn)證。

form.hidden_tag() 模板參數(shù)生成了一個隱藏字段,包含防止表單遭受CSFR攻擊的令牌。要保護(hù)表單你要做的就是包含上這個隱藏字段并在Flask配置中設(shè)置好SECRET_KEY變量。如果你做好這兩件事,剩下的Flask-WTF會統(tǒng)統(tǒng)幫你搞定。

如果你過去編寫過HTML表單,你會發(fā)現(xiàn)奇怪的是模板中并沒有HTML字段。這是因?yàn)閬碜耘c表單對象的字段們知道如何把自己轉(zhuǎn)換成HTML。在需要字段標(biāo)簽的地方,我只需要添加 {{ form.<field_name>.label }} 標(biāo)記,在需要字段的地方包含上 {{ form.<field_name>() }} 。 如果字段需要附加其他HTML屬性,可以以參數(shù)形式傳遞過來。模板中的 username 和 password 字段就獲取了size參數(shù),作為屬性把這一參數(shù)添加給 <input> HTML 元素。同樣,你也可以給表單字段添加CSS類或IDs。

表單視圖

現(xiàn)在只差最后一步,就能在瀏覽器里看到這個表單了——那就是為應(yīng)用程序編寫新的視圖代碼來渲染前一節(jié)的模板。

因此,我們編寫一個新視圖函數(shù)映射到 /login URL,我們將差ungjian表單并傳遞給模板來渲染之。這個視圖函數(shù)和以前的那些存在一個模塊里 app/routes.py

app/routes.py: 登錄視圖函數(shù)

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 類,由其實(shí)例化一個對象,然后將該對象發(fā)送給模板。 form=form這個語法看著怪怪的,其實(shí)它只是簡單的把上面創(chuàng)建的form對象(“=” 右邊這個)用變量名字form(“=”左邊這個)傳遞給模板。它包含了要渲染的所有字段。
(這樣在模板中,使用變量名‘form’來調(diào)用渲染各字段,如果你把‘=‘左邊改成拼音’biaodan‘,那在模板中就得使用‘biaodan.username'這樣的形式調(diào)用了。)

為了易于訪問登錄表單,我們在base模板中的導(dǎo)航欄上添加一個指向它的鏈接:

app/templates/base.html: 導(dǎo)航欄里的登錄連接

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

現(xiàn)在,你就可以運(yùn)行程序并實(shí)際看到瀏覽器里的表單了。程序運(yùn)行起來以后,在地址欄中輸入 http://localhost:5000/ ,然后在頂部的導(dǎo)航欄點(diǎn)擊"Login", 就能看到嶄新的登錄界面了。相當(dāng)酷,對吧?

登錄表單

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

如果你試圖點(diǎn)擊提交按鈕,瀏覽器將顯示 "Method Not Allowed" (方法未允許)錯誤。這是因?yàn)樯瞎?jié)的視圖函數(shù)的工作只做了半截。它只在頁面上顯示了表單,但并沒有處理用戶提交數(shù)據(jù)的邏輯處理代碼。這是Flask-WTF簡化工作的另一個表現(xiàn)了。下面是更新版本的視圖函數(shù),它將接收并驗(yàn)證用戶提交的數(shù)據(jù):

app/routes.py: 接收登錄驗(yàn)證

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)

這一版的第一個新看點(diǎn)是路由裝飾器參數(shù)中的 methods 參數(shù)。它告訴Flask 這個視圖函數(shù)接受 GETPOST 請求,覆蓋掉了默認(rèn)的只接受 GET 請求。 HTTP 協(xié)議的GET請求狀態(tài)是返回信息給客戶端(本例中是瀏覽器)。 到目前為止,程序中所有請求都是這種類型。 POST 請求通常在瀏覽器傳送表單數(shù)據(jù)給服務(wù)器時使用。(實(shí)際上,GET請求也能實(shí)現(xiàn)這一功能,但不是一個很好的推薦)。 前面瀏覽器返回給你的錯誤 "Method Not Allowed" ,表明瀏覽器試圖用 POST 方法發(fā)送數(shù)據(jù),但在程序中并沒有配置接受該方法——那么,通過添加 methods 參數(shù),F(xiàn)lask就知道應(yīng)該接受哪一種方法了。

form.validate_on_submit() 方法會完成所有表單處理的工作。當(dāng)瀏覽器發(fā)送 GET 請求來接收頁面的表單數(shù)據(jù)時,這個方法(validate_on_submit())將返回FALSE。因此這種情況下,驗(yàn)證表單數(shù)據(jù)函數(shù)跳過了if語句下面的內(nèi)容,在視圖函數(shù)的最后一行直接進(jìn)行渲染模板。

當(dāng)用戶點(diǎn)擊提交按鈕,瀏覽器發(fā)送 POST 請求時,form.validate_on_submit() 就會收集所有數(shù)據(jù),運(yùn)行所有字段上附加的有效性驗(yàn)證器,如果所有驗(yàn)證無誤,它就會返回True,表明所有數(shù)據(jù)是有效的,可以被程序處理。但如果任何一個字段值驗(yàn)證失敗,這個函數(shù)就會返回 False,這將重新渲染此表單并回發(fā)給用戶, 類似與GET 請求那樣。稍后,我們將添加一個驗(yàn)證失敗的錯誤信息。

當(dāng) form.validate_on_submit() 返回 True,登錄(login)視圖函數(shù)將調(diào)用從Flask導(dǎo)入的兩個新函數(shù)。 flash() 函數(shù)是給用戶顯示信息很有用的方法。相當(dāng)多的程序都使用這個技術(shù)告知用戶其操作成功或者失敗。這里,我將使用這個機(jī)制作為臨時解決方案,因?yàn)槲覀兡壳斑€沒有供真實(shí)用戶登錄的基礎(chǔ)結(jié)構(gòu)。目前我最多也就是顯示程序確認(rèn)接受到了認(rèn)證的信息。

登錄視圖函數(shù)所用到的第二個新函數(shù)是 redirect()。 這個函數(shù)通過給定一個不同頁面的參數(shù),指示客戶端自動導(dǎo)航過去。 此處,這個視圖函數(shù)用它來把用戶重新定向到程序的首頁。

當(dāng)你調(diào)用 flash() 函數(shù), Flask會存儲該信息,但要閃現(xiàn)的信息并不會自動出現(xiàn)在頁面上。 程序的模板需要在模板布局上渲染那些閃現(xiàn)信息才行。我將把這個結(jié)構(gòu)添加到基礎(chǔ)模板,這樣所有模板頁面都會繼承得到這一功能。這是更新后的基礎(chǔ)模板:

app/templates/base.html: 在基礎(chǔ)模板中閃現(xiàn)信息

<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)把調(diào)用get_flashed_messages()得到的所有結(jié)果關(guān)聯(lián)給 messages 變量, 所有這些都在模板的上下文里。 來自于 Flask的函數(shù)get_flashed_messages() 會返回一個前面由flash()注冊的信息列表。 接下來的條件語句檢查 messages 中是否有內(nèi)容,在這, 會把每一條信息作為一個<li>列表項(xiàng),然后組合成元素 <ul> 列表。這種呈現(xiàn)風(fēng)格并不很好,稍后我們將介紹如何風(fēng)格化整個應(yīng)用程序(使用CSS)。

有趣的是,這些被閃現(xiàn)的信息,一旦通過 get_flashed_messages 函數(shù)請求調(diào)用后,就會被從信息列表中清除,所以他們只會在調(diào)用flash()函數(shù)后出現(xiàn)一次。

這是一個偉大的時刻:不斷嘗試程序,測試表單是如何工作的。確保你提交的表單中使用了空白的用戶名和|或密碼提交,這樣你就會看到DataRequired驗(yàn)證器是如何 掛起 提交進(jìn)程的。

改進(jìn)字段驗(yàn)證

附加于表單字段的驗(yàn)證器目的就是阻止接受到的無效數(shù)據(jù)進(jìn)入程序。 應(yīng)用程序處理表單輸入的方式是重新顯示表單,以供用戶進(jìn)行必要的修改。

如果你嘗試過提交無效數(shù)據(jù),你肯定已經(jīng)注意到如果驗(yàn)證機(jī)制工作良好,并沒有給用戶“表單有錯誤”的提示,用戶只會單純的看到表單重新出現(xiàn)。接下來的任務(wù)是改善用戶體驗(yàn),給每個驗(yàn)證失敗的字段添加上有意義的錯誤提示信息。

事實(shí)上,表單驗(yàn)證器已經(jīng)生成了相關(guān)的錯誤描述信息,所以我們?nèi)鄙俚木褪翘砑右恍┻壿嬙谀0逯酗@示就可以了。

下面是帶有用戶名和密碼驗(yàn)證信息的登錄模板:

app/templates/login.html: 驗(yàn)證錯誤信息的login模板

{% 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 %}

我所做的更改就是在用戶名和字段之后添加for循環(huán)來顯示錯誤信息——紅色字體。作為通用規(guī)則,任何帶有驗(yàn)證器的字段的錯誤信息都會添加到 form.<field_name>.errors下面。這是一個 list列表(可能包含多條而非一條信息),因?yàn)樽侄慰赡苡卸鄺l驗(yàn)證器,也就可能會有不知一條的信息要顯示給用戶。

如果你試圖提交空用戶名或密碼的表單,你將看到紅色的錯誤信息。

Form validation

生成鏈接

現(xiàn)在,登錄表單已經(jīng)完成The login form is fairly complete now, but before closing this chapter I wanted to discuss the proper way to include links in templates and redirects. So far you have seen a few instances in which links are defined. For example, this is the current navigation bar in the base template:

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

The login view function also defines a link that is passed to the redirect() function:

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

One problem with writing links directly in templates and source files is that if one day you decide to reorganize your links, then you are going to have to search and replace these links in your entire application.

To have better control over these links, Flask provides a function called url_for(), which generates URLs using its internal mapping of URLs to view functions. For example, url_for('login') returns /login, and url_for('index') return '/index. The argument to url_for() is the endpoint name, which is the name of the view function.

You may ask why is it better to use the function names instead of URLs. The fact is that URLs are much more likely to change than view function names, which are completely internal. A secondary reason is that as you will learn later, some URLs have dynamic components in them, so generating those URLs by hand would require concatenating multiple elements, which is tedious and error prone. The url_for() is also able to generate these complex URLs.

So from now on, I'm going to use url_for() every time I need to generate an application URL. The navigation bar in the base template then becomes:

app/templates/base.html: Use url_for() function for links

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

And here is the updated login() view function:

app/routes.py: Use url_for() function for links

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輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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