1. 程式人生 > >《Flask 入門教程》第 7 章:表單

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

在 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 程式碼有下面幾點需要注意:

  • <form> 標籤裡使用 method 屬性將提交表單資料的 HTTP 請求方法指定為 POST。如果不指定,則會預設使用 GET 方法,這會將表單資料通過 URL 提交,容易導致資料洩露,而且不適用於包含大量資料的情況。
  • <input> 元素必須要指定 name 屬性,否則無法提交資料,在伺服器端,我們也需要通過這個 name 屬性值來獲取對應欄位的資料。

提示 填寫輸入框標籤文字的 <label> 元素不是必須的,只是為了輔助滑鼠使用者。當使用滑鼠點選標籤文字時,會自動啟用對應的輸入框,這對複選框來說比較有用。for

屬性填入要繫結的 <input> 元素的 id 屬性值。

建立新條目

建立新條目可以放到一個新的頁面來實現,也可以直接在主頁實現。這裡我們採用後者,首先在主頁模板裡新增一個表單:

templates/index.html:新增建立新條目表單

<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 標誌屬性,如果使用者沒有輸入內容就按下了提交按鈕,瀏覽器會顯示錯誤提示。

兩個輸入框和提交按鈕相關的 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 請求。

提示 在 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