第4章:Web表单-Flask Web 开发(第2版)

到目前为止,我们所学习的都是单向信息:信息都是从服务器单向地输出给用户。然而,大部分应用都需要另一个方向的信息——从用户那里获取信息。

通过 HTML 的表单,用户可以输入信息,然后提交到服务器。通常,用户通过表单输入的信息都是以 POST 方式提交到服务器的。在 Flask 中,使用 request.form 获取用户 POST 过来的数据。

尽管 Flask 的 request 对象中提供的支持足以处理 Web 表单,但有一些任务可能变得繁琐和重复。比如,既要编写 HTML 表单的代码,又要对用户通过表单提交的数据进行验证。

而 Flask-WTF 扩展,可以简化这个过程。这个扩展对与框架无关的 WTForms 进行包装,并将其整合至 Flask。

Flask-WTF 扩展及其所依赖的包,可以通过一条 pip 命令来轻松完成安装:

(venv) $ pip install flask-wtf

配置

和其它的 Flask 扩展不同,Flask-WTF 不需要使用应用对象进行初始化,但是它需要应用程序对象配置了 secret key (密钥)选项。

# 示例 4-1. hello.py: 配置 Flask-WTF(所需的 SECRET_KEY)
app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'

app.config 是一个字典,它存储了 Flask 应用程序自身和 Flask 扩展所需要使用的配置。

Flask-WTF 之所以需要在应用程序中配置密钥,是因为此密钥是扩展程序用来保护所有表单免受跨站请求伪造(CSRF)攻击机制的一部分。当恶意网站向用户当前登录的应用程序服务器发送请求时,会触发 CSRF 攻击。Flask-WTF 为所有表单生成安全令牌,并将该令牌保存在用户当前的会话中,用户提交表单时,Flask-WTF 会对令牌进行检测,只有合法的令牌才能提交表单。

表单类

一旦使用了 Flask-WTF,所有的表单都需要在服务端编写继承自 FlaskForm 基类的表单类。表单类定义了该表单所需的所有表单字段。在使用时,只需实例化该表单类。每个表单字段都可以有若干 validators(验证器) 属性。验证器用于对用户提交的数据进行验证,它是一个函数。

示例 4-2 展示了一个包含单行文本框和提交按钮的表单类。

# 示例 4-2. hello.py: 定义表单类
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired

class NameForm(FlaskForm):
    name = StringField('你叫什么名字?', validators=[DataRequired()])
    submit = SubmitField('提交')

下表列示了 WTForms 所支持的标准 HTML 表单元素。

表单元素 说明
BooleanField 值为 TrueFalse 的复选框(checkbox)
DateField 接收 datetime.date 值的文本框
DateTimeField 接收datetime.datetime值的文本框
DecimalField 接收decimal.Decimal值的文本框
FileField 文件上传框
HiddenField 隐藏域
MutipleFileField 多文件上传框
FieldList 给定类型的字段列表
FloatField 接收浮点数值的文本框
FormField 嵌套表单
IntegerField 接收整数值的文本框
PasswordField 密码框
RadioField 单选框
SelectField 下拉列表框
MutipleSelectField 可多选的下拉列表框
SubmitField 提交按钮
StringField 单行文本框
TextAreaField 多行文本框

下表列示了 WTForms 内置的验证器。

验证器 说明
DataRequired 必填(表单提交后验证)
Email 必须是一个 Email 地址
EqualTo 对两个表单元素的值进行相等性判断
InputRequired 必填(表单提交前验证)
IPAddress 必须是一个 IP 地址(ipv4)
Length 验证提交内容的长度
MacAddress 必须是一个 MAC 地址
NumberRange 必须在一个范围内的数字
Optional 可选项,提交的内容可以为空
Regexp 使用正则表达式对提交的内容进行验证
URL 必须是一个 URL
UUID 必须是一个 UUID
AnyOf 提交的值必须是列表值中的一个
NoneOf 提交的值必须不是列表值中的一个

在 HTML 中渲染表单

在视图函数中,实例化示例4-2中定义的 NameForm 类,并将其命名为 form,传递给模板。模板就可以使用该表单,并进行渲染了。

<form method="POST">
    {{ form.hidden_tag() }}
    {{ form.name.label }} {{ form.name() }}
    {{ form.submit() }}
</form>

注意,上面代码中,除了使用了表单类定义的 namesubmit 两个元素外,还使用了额外的 form.hidden_tag()。它是 Flask-WTF 用于防范 CSRF 攻击而生成的令牌,并以隐藏域的形式放置在 HTML 表单中。

我们还可以在渲染表单元素时,通过关键字参数来指定该元素的 HTML 属性。下例中,为 name 元素指定了 id="my-text-field" 属性:

<form method="POST">
    {{ form.hidden_tag() }}
    {{ form.name.label }} {{ form.name(id='my-text-field') }}
    {{ form.submit() }}
</form>

Flask-Bootstrap 提供了一个助手工具,来将 Flask-WTF 表单无缝地渲染为 Bootstrap 样式。上面的例子可以使用下面的代码实现快速渲染。

{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}

使用 Flask-WTF 和 Flask-Bootstrap 来渲染表单的完整的模板代码如下所示:

<!--示例 4-3. templates/index.html: 使用 Flask-WTF 和 Flask-Bootstrap 来渲染表单-->
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>你好, {% if name %}{{ name }}{% else %}陌生人{% endif %}!</h1>
</div>
{{ wtf.quick_form(form) }}
{% endblock %}

在视图函数中处理表单

在新版的 hello.py 中,index() 视图函数有两件事要做。首先要渲染表单,然后处理通过表单提交的数据。

# 示例 4-4. hello.py: 使用 GET 和 POST 请求方式来显示并处理 web 表单
@app.route('/', methods=['GET', 'POST'])
def index():
    name = None
    form = NameForm()
    if form.validate_on_submit():
        name = form.name.data
        form.name.data = ''
    return render_template('index.html', form=form, name=name)

app.route 装饰器中,加上 methods 参数,是为了让该视图函数具备同时处理 GETPOST 两种请求的能力。

局部变量 name 保存了从表单提交过来的用户姓名。当表单通过 POST 提交并通过了所有验证之后,validate_on_submit() 方法将返回 True。用户第一次通过浏览器导航到该 URL 时,此时的请求方法是 GET,validate_on_submit() 方法返回 False

如果你从 Github 克隆了示例代码,可以使用 git checkout 4a 签出本示例。

重定向和用户会话

这个版本的 hello.py 有一个问题。当用户提交表单后,点击浏览器的“刷新”按钮,浏览器将会弹出一个“是否重新提交”的警告。原因在于,最后一次请求是 POST。浏览器的这个警告可能会让用户感到不解,所以我们要把这个问题进行处理。处理方法很简单:将最后一次请求变成 GET。

通过 redirect(重定向) 可以实现上述功能。然而,新问题又来了。form.name.data 只有在 POST 请求中才能拿到,这意味着重定向后,该数据将会丢失。

为了实现多次请求间的数据共享,我们需要借助 session(会话)。在 Flask 中,Session 的操作类似字典。

# 示例 4-5. hello.py: 重定向和用户会话
from flask import Flask, render_template, session, redirect, url_for

@app.route('/', methods=['GET', 'POST'])
def index():
    form = NameForm()
    if form.validate_on_submit():
        # 在 POST 请求里,将用户输入的内容保存到 Session 中
        session['name'] = form.name.data
        return redirect(url_for('index'))
    # 在 GET 请求里,将保存在 Session 中的用户输入内容,传递给模板
    return render_template('index.html', form=form, name=session.get('name'))

如果你从 Github 克隆了示例代码,可以使用 git checkout 4b 签出本示例。

消息闪现

通常用于在用户完成某个操作后,以文本消息的方式提醒用户。Flask 以核心函数的方式对此进行支持,该函数叫做 flash

# 示例 4-6. hello.py: 消息闪现
from flask import Flask, render_template, session, redirect, url_for, flash

@app.route('/', methods=['GET', 'POST'])
def index():
    form = NameForm()
    if form.validate_on_submit():
        old_name = session.get('name')
        if old_name is not None and old_name != form.name.data:
            # 设置消息
            flash('看样子,你修改了姓名!')
        session['name'] = form.name.data
        return redirect(url_for('index'))
    return render_template('index.html', form = form, name = session.get('name'))

在视图函数中,通过 flash() 函数设置消息。之后,需要在模板里将这些消息显示出来。Jinja2 提供了 get_flashed_message() 函数来完成此操作。

<!-- 示例 4-7. templates/base.html: 渲染闪现消息 -->
{% block content %}
<div class="container">
    {% for message in get_flashed_messages() %}
    <div class="alert alert-warning">
        <button type="button" class="close" data-dismiss="alert">&times;</button>
        {{ message }}
    </div>
    {% endfor %}

    {% block page_content %}{% endblock %}
</div>
{% endblock %}

如果你从 Github 克隆了示例代码,可以使用 git checkout 4c 签出本示例。

本文并非《Flask Web Development(2nd edition)》的全文翻译。而只是在阅读该书时,对自认为重点的内容进行记录以及思考。由于水平有限,本文所述内容难免出现不足或错误。请通过购买正版图书来进行更系统地学习,原书地址。本站将依照本系列文章录制视频教程,届时本文未作记录的部分以及项目实际开发中所需要的知识都会在视频教程中进行补充。

flask Flask Web Development(2nd edition) 2018-03-25 23:02 2547105