《Flask 入門教程》第 7 章:表單

在 HTML 頁面裏,咱們須要編寫表單來獲取用戶輸入。一個典型的表單以下所示:html

<form method="post">  <!-- 指定提交方法爲 POST -->
    <label for="name">名字</label>
    <input type="text" name="name" id="name"><br>  <!-- 文本輸入框 -->
    <label for="occupation">職業</label>
    <input type="text" name="occupation" id="occupation"><br>  <!-- 文本輸入框 -->
    <input type="submit" name="submit" value="登陸">  <!-- 提交按鈕 -->
</form>複製代碼

編寫表單的 HTML 代碼有下面幾點須要注意:git

  • <form> 標籤裏使用 method 屬性將提交表單數據的 HTTP 請求方法指定爲 POST。若是不指定,則會默認使用 GET 方法,這會將表單數據經過 URL 提交,容易致使數據泄露,並且不適用於包含大量數據的狀況。
  • <input> 元素必需要指定 name 屬性,不然沒法提交數據,在服務器端,咱們也須要經過這個 name 屬性值來獲取對應字段的數據。

提示 填寫輸入框標籤文字的 <label> 元素不是必須的,只是爲了輔助鼠標用戶。當使用鼠標點擊標籤文字時,會自動激活對應的輸入框,這對複選框來講比較有用。for 屬性填入要綁定的 <input> 元素的 id 屬性值。github

建立新條目

建立新條目能夠放到一個新的頁面來實現,也能夠直接在主頁實現。這裏咱們採用後者,首先在主頁模板裏添加一個表單:數據庫

templates/index.html:添加建立新條目表單flask

<p>{{ movies|length }} Titles</p>
<form method="post">
    Name <input type="text" name="name" autocomplete="off" required>
    Year <input type="text" name="year" autocomplete="off" required>
    <input class="btn" type="submit" name="submit" value="Add">
</form>複製代碼

在這兩個輸入字段中,autocomplete 屬性設爲 off 來關閉自動完成(按下輸入框不顯示歷史輸入記錄);另外還添加了 required 標誌屬性,若是用戶沒有輸入內容就按下了提交按鈕,瀏覽器會顯示錯誤提示。bootstrap

兩個輸入框和提交按鈕相關的 CSS 定義以下:瀏覽器

/* 覆蓋某些瀏覽器對 input 元素定義的字體 */
input[type=submit] {
    font-family: inherit;
}

input[type=text] {
    border: 1px solid #ddd;
}

input[name=year] {
    width: 50px;
}

.btn {
    font-size: 12px;
    padding: 3px 5px;
    text-decoration: none;
    cursor: pointer;
    background-color: white;
    color: black;
    border: 1px solid #555555;
    border-radius: 5px;
}

.btn:hover {
    text-decoration: none;
    background-color: black;
    color: white;
    border: 1px solid black;
}複製代碼

接下來,咱們須要考慮如何獲取提交的表單數據。安全

處理表單數據

默認狀況下,當表單中的提交按鈕被按下,瀏覽器會建立一個新的請求,默認發往當前 URL(在 <form> 元素使用 action 屬性能夠自定義目標 URL)。服務器

由於咱們在模板裏爲表單定義了 POST 方法,當你輸入數據,按下提交按鈕,一個攜帶輸入信息的 POST 請求會發往根地址。接着,你會看到一個 405 Method Not Allowed 錯誤提示。這是由於處理根地址請求的 index 視圖默認只接受 GET 請求。session

提示 在 HTTP 中,GET 和 POST 是兩種最多見的請求方法,其中 GET 請求用來獲取資源,而 POST 則用來建立 / 更新資源。咱們訪問一個連接時會發送 GET 請求,而提交表單一般會發送 POST 請求。

爲了可以處理 POST 請求,咱們須要修改一下視圖函數:

@app.route('/', methods=['GET', 'POST'])複製代碼

app.route() 裝飾器裏,咱們能夠用 methods 關鍵字傳遞一個包含 HTTP 方法字符串的列表,表示這個視圖函數處理哪一種方法類型的請求。默認只接受 GET 請求,上面的寫法表示同時接受 GET 和 POST 請求。

兩種方法的請求有不一樣的處理邏輯:對於 GET 請求,返回渲染後的頁面;對於 POST 請求,則獲取提交的表單數據並保存。爲了在函數內加以區分,咱們添加一個 if 判斷:

app.py:建立電影條目

from flask import request, url_for, redirect, flash

# ...

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':  # 判斷是不是 POST 請求
        # 獲取表單數據
        title = request.form.get('title')  # 傳入表單對應輸入字段的 name 值
        year = request.form.get('year')
        # 驗證數據
        if not title or not year or len(year) > 4 or len(title) > 60:
            flash('Invalid input.')  # 顯示錯誤提示
            return redirect(url_for('index'))  # 重定向回主頁
        # 保存表單數據到數據庫
        movie = Movie(title=title, year=year)  # 建立記錄
        db.session.add(movie)  # 添加到數據庫會話
        db.session.commit()  # 提交數據庫會話
        flash('Item Created.')  # 顯示成功建立的提示
        return redirect(url_for('index'))  # 重定向回主頁

    user = User.query.first()
    movies = Movie.query.all()
    return render_template('index.html', user=user, movies=movies)複製代碼

if 語句內,咱們編寫了處理表單數據的代碼,其中涉及 3 個新的知識點,下面來一一瞭解。

請求對象

Flask 會在請求觸發後把請求信息放到 request 對象裏,你能夠從 flask 包導入它:

from flask import request複製代碼

由於它在請求觸發時纔會包含數據,因此你只能在視圖函數內部調用它。它包含請求相關的全部信息,好比請求的路徑(request.path)、請求的方法(request.method)、表單數據(request.form)、查詢字符串(request.args)等等。

在上面的 if 語句中,咱們首先經過 request.method 的值來判斷請求方法。在 if 語句內,咱們經過 request.form 來獲取表單數據。request.form 是一個特殊的字典,用表單字段的 name 屬性值能夠獲取用戶填入的對應數據:

if request.method == 'POST':
    title = request.form.get('title')
    year = request.form.get('year')複製代碼

flash 消息

在用戶執行某些動做後,咱們一般在頁面上顯示一個提示消息。最簡單的實現就是在視圖函數裏定義一個包含消息內容的變量,傳入模板,而後在模板裏渲染顯示它。由於這個需求很經常使用,Flask 內置了相關的函數。其中 flash() 函數用來在視圖函數裏向模板傳遞提示消息,get_flashed_messages() 函數則用來在模板中獲取提示消息。

flash() 的用法很簡單,首先從 flask 包導入 flash 函數:

from flask import flash複製代碼

而後在視圖函數裏調用,傳入要顯示的消息內容:

flash('Item Created.')複製代碼

flash() 函數在內部會把消息存儲到 Flask 提供的 session 對象裏。session 用來在請求間存儲數據,它會把數據簽名後存儲到瀏覽器的 Cookie 中,因此咱們須要設置簽名所需的密鑰:

app.config['SECRET_KEY'] = 'dev'  # 等同於 app.secret_key = 'dev'複製代碼

提示 這個密鑰的值在開發時能夠隨便設置。基於安全的考慮,在部署時應該設置爲隨機字符,且不該該明文寫在代碼裏, 在部署章節會詳細介紹。

下面在基模板(base.html)裏使用 get_flashed_messages() 函數獲取提示消息並顯示:

<!-- 插入到頁面標題上方 -->
{% for message in get_flashed_messages() %}
	<div class="alert">{{ message }}</div>
{% endfor %}
<h2>...</h2>複製代碼

alert 類爲提示消息增長樣式:

.alert {
    position: relative;
    padding: 7px;
    margin: 7px 0;
    border: 1px solid transparent;
    color: #004085;
    background-color: #cce5ff;
    border-color: #b8daff;
    border-radius: 5px;
}複製代碼

經過在 <input> 元素內添加 required 屬性實現的驗證(客戶端驗證)並不徹底可靠,咱們還要在服務器端追加驗證:

if not title or not year or len(year) > 4 or len(title) > 60:
    flash('Invalid input.')  # 顯示錯誤提示
    return redirect(url_for('index'))
# ...
flash('Item Created.')  # 顯示成功建立的提示複製代碼

提示 在真實世界裏,你會進行更嚴苛的驗證,好比對數據去除首尾的空格。通常狀況下,咱們會使用第三方庫(好比 WTForms)來實現表單數據的驗證工做。

若是輸入的某個數據爲空,或是長度不符合要求,就顯示錯誤提示「Invalid input.」,不然顯示成功建立的提示「Item Created.」。

重定向響應

重定向響應是一類特殊的響應,它會返回一個新的 URL,瀏覽器在接受到這樣的響應後會向這個新 URL 再次發起一個新的請求。Flask 提供了 redirect() 函數來快捷生成這種響應,傳入重定向的目標 URL 做爲參數,好比 redirect('http://helloflask.com')

根據驗證狀況,咱們發送不一樣的提示消息,最後都把頁面重定向到主頁,這裏的主頁 URL 均使用 url_for() 函數生成:

if not title or not year or len(year) > 4 or len(title) > 60:
    flash('Invalid title or year!')  
    return redirect(url_for('index'))  # 重定向回主頁
flash('Movie Created!')
return redirect(url_for('index'))  # 重定向回主頁複製代碼

編輯條目

編輯的實現和建立相似,咱們先建立一個用於顯示編輯頁面和處理編輯表單提交請求的視圖函數:

app.py:編輯電影條目

@app.route('/movie/edit/<int:movie_id>', methods=['GET', 'POST'])
def edit(movie_id):
    movie = Movie.query.get_or_404(movie_id)

    if request.method == 'POST':  # 處理編輯表單的提交請求
        title = request.form['title']
        year = request.form['year']
        
        if not title or not year or len(year) > 4 or len(title) > 60:
            flash('Invalid input.')
            return redirect(url_for('edit', movie_id=movie_id))  # 重定向回對應的編輯頁面
        
        movie.title = title  # 更新標題
        movie.year = year  # 更新年份
        db.session.commit()  # 提交數據庫會話
        flash('Item Updated.')
        return redirect(url_for('index'))  # 重定向回主頁
    
    return render_template('edit.html', movie=movie)  # 傳入被編輯的電影記錄複製代碼

這個視圖函數的 URL 規則有一些特殊,若是你還有印象的話,咱們在第 2 章的《實驗時間》部分曾介紹過這種 URL 規則,其中的 <int:movie_id> 部分表示 URL 變量,而 int 則是將變量轉換成整型的 URL 變量轉換器。在生成這個視圖的 URL 時,咱們也須要傳入對應的變量,好比 url_for('edit', movie_id=2) 會生成 /movie/edit/2。

movie_id 變量是電影條目記錄在數據庫中的主鍵值,這個值用來在視圖函數裏查詢到對應的電影記錄。查詢的時候,咱們使用了 get_or_404() 方法,它會返回對應主鍵的記錄,若是沒有找到,則返回 404 錯誤響應。

爲何要在最後把電影記錄傳入模板?既然咱們要編輯某個條目,那麼必然要在輸入框裏提早把對應的數據放進去,以便於進行更新。在模板裏,經過表單 <input> 元素的 value 屬性便可將它們提早寫到輸入框裏。完整的編輯頁面模板以下所示:

templates/edit.html:編輯頁面模板

{% extends 'base.html' %}

{% block content %}
<h3>Edit item</h3>
<form method="post">
    Name <input type="text" name="title" autocomplete="off" required value="{{ movie.title }}">
    Year <input type="text" name="year" autocomplete="off" required value="{{ movie.year }}">
    <input class="btn" type="submit" name="submit" value="Update">
</form>
{% endblock %}複製代碼

最後在主頁每個電影條目右側都添加一個指向該條目編輯頁面的連接:

index.html:編輯電影條目的連接

<span class="float-right">
    <a class="btn" href="{{ url_for('edit', movie_id=movie.id) }}">Edit</a>
    ...
</span>複製代碼

點擊某一個電影條目的編輯按鈕打開的編輯頁面以下圖所示:



刪除條目

由於不涉及數據的傳遞,刪除條目的實現更加簡單。首先建立一個視圖函數執行刪除操做,以下所示:

app.py:刪除電影條目

@app.route('/movie/delete/<int:movie_id>', methods=['POST'])  # 限定只接受 POST 請求
def delete(movie_id):
    movie = Movie.query.get_or_404(movie_id)  # 獲取電影記錄
    db.session.delete(movie)  # 刪除對應的記錄
    db.session.commit()  # 提交數據庫會話
    flash('Item Deleted.')
    return redirect(url_for('index'))  # 重定向回主頁複製代碼

爲了安全的考慮,咱們通常會使用 POST 請求來提交刪除請求,也就是使用表單來實現(而不是建立刪除連接):

index.html:刪除電影條目表單

<span class="float-right">
    ...
    <form class="inline-form" method="post" action="{{ url_for('delete', movie_id=movie.id) }}">
        <input class="btn" type="submit" name="delete" value="Delete" onclick="return confirm('Are you sure?')">
    </form>
    ...
</span>複製代碼

爲了讓表單中的刪除按鈕和旁邊的編輯連接排成一行,咱們爲表單元素添加了下面的 CSS 定義:

.inline-form {
    display: inline;
}複製代碼

最終的程序主頁以下圖所示:



本章小結

本章咱們完成了程序的主要功能:添加、編輯和刪除電影條目。結束前,讓咱們提交代碼:

$ git add .
$ git commit -m "Create, edit and delete item by form"
$ git push複製代碼

提示 你能夠在 GitHub 上查看本書示例程序的對應 commit:f7f7355。在後續的 commit 裏,咱們爲另外兩個常見的 HTTP 錯誤:400(Bad Request) 和 500(Internal Server Error) 錯誤編寫了錯誤處理函數和對應的模板,前者會在請求格式不符要求時返回,後者則會在程序內部出現任意錯誤時返回(關閉調試模式的狀況下)。

進階提示

  • 從上面的代碼能夠看出,手動驗證表單數據既麻煩又不可靠。對於複雜的程序,咱們通常會使用集成了 WTForms 的擴展 Flask-WTF 來簡化表單處理。經過編寫表單類,定義表單字段和驗證器,它能夠自動生成表單對應的 HTML 代碼,並在表單提交時驗證表單數據,返回對應的錯誤消息。更重要的,它還內置了 CSRF(跨站請求僞造) 保護功能。你能夠閱讀 Flask-WTF 文檔和 Hello, Flask! 專欄上的表單系列文章瞭解具體用法。
  • CSRF 是一種常見的攻擊手段。以咱們的刪除表單爲例,某惡意網站的頁面中內嵌了一段代碼,訪問時會自動發送一個刪除某個電影條目的 POST 請求到咱們的程序。若是咱們訪問了這個惡意網站,就會致使電影條目被刪除,由於咱們的程序無法分辨請求發自哪裏。解決方法一般是在表單裏添加一個包含隨機字符串的隱藏字段,在提交時經過對比這個字段的值來判斷是不是用戶本身發送的請求。在咱們的程序中沒有實現 CSRF 保護。
  • 使用 Flask-WTF 時,表單類在模板中的渲染代碼基本相同,你能夠編寫宏來渲染表單字段。若是你使用 Bootstap,那麼擴展 Bootstrap-Flask 內置了多個表單相關的宏,能夠簡化渲染工做。
  • 你能夠把刪除按鈕的行內 JavaScript 代碼改成事件監聽函數,寫到單獨的 JavaScript 文件裏。
  • 《Flask Web 開發實戰》第 4 章介紹了表單處理的各個方面,包括表單類的編寫和渲染、錯誤消息顯示、自定義錯誤消息語言、文件和多文件上傳、富文本編輯器等等。
  • 本書主頁 & 相關資源索引:http://helloflask.com/tutorial
相關文章
相關標籤/搜索