基于Python的Web应用开发实战——3 Web表单
第2章中介绍的 请求对象 包含客户端发出的所有请求信息。
其中, request.form 能获取 POST请求 中提交的表单数据。
尽管Flask的请求对象提供的信息足够用于处理Web表单,但有些任务很单调,而且要重复操作。
比如,生成表单的HTML代码和验证提交的表单数据。
Flask-WTF(https://flask-wtf.readthedocs.io/en/stable/)扩展可以把处理Web表单的过程变成一种愉悦的体验。
这个扩展对独立的WTForms(https://wtforms.readthedocs.io/en/stable/)包进行了包装,方便集成到Flask程序中。
Flask_WTF 及其依赖可以使用pip安装:
pip install flask-wtf
4.1 跨站请求伪造保护
默认情况下,Flask-WTF能保护所有表单免受跨站请求伪造(Cross-Site Request Forgery)的攻击。
恶意网站把请求发送到被攻击者已登录的其他网站时就会引发CSRF攻击。
为了实现CSRF保护,Flask-WTF需要在程序设置一个密钥。
Flask-WTF 使用这个密钥生成加密令牌,再用令牌验证请求中表单数据的真伪。
Flask-WTF 设置密钥的方法如示例4-1
示例 4-1 hello.py:设置Flask-WTF
app = Flask(__name__) app.config["SECRET_KEY"] = "hard to guess string"
app.config 字典用来存储框架、扩展和程序本身的配置变量。
使用标准字典句法就能把配置值添加到 app.config 对象中。
这个对象还提供了一些方法,可以从文件或环境中导入配置。
SECRET_KEY 配置变量是通用密钥,可在Flask 和多个第三方扩展中使用。
如其名所示,加密的强度取决于变量值的机密程序。
不同的程度要使用不同的密钥,而且,要保证其他人不知道你所用的字符串。
为了增强安全性,密钥不应该直接写入代码,而要保存在环境变量中。
这一个技术会在第7章介绍
4.2 表单类
使用Flask-WTF时,每个Web表单都由一个继承自Form的类表示。
这个类定义表单中的一组字段,每个字段都用对象表示。
字段对象可附属一个或多个验证函数。
验证函数用来验证用户提交的输入值是否符合要求。
示例 4-2 是一个简单的Web表单,包含一个文本字段和一个提交按钮。
示例 4-2 hellop.py:定义表单类
from flask_wtf import FlaskForm from wtforms import StringField,SubmitField from wtforms.validators import Required class NameForm(FlaskForm): name = StringField("What is your name?",validators=[Required()]) submit = SubmitField("Submit")
这个表单中的字段定义为类变量,类变量的值是相应字段类型的对象。
在这个示例中,NameForm 表单中有一个名为 name的文本字段和一个名为submit的提交按钮。
StringField 类表示属性为 type="text" 的<input>元素。
SubmitField 类表示属性为 type="submit"的 <input>元素。
字段构造函数的第一个参数是把表单渲染成HTML时使用的符号。
StringFiled 构造函数中的可选参数 validators 指定一个由验证函数组成的列表,在接受用户提交的数据之前验证数据。
验证函数 Required() 确保提交的字段不为空。
Form 基类由 Flask_WTF扩展定义,所以从flask_wtf 中导入。
字段和验证函数却可以直接从 WTForms 包中导入。
WTForms支持的HTML标准字段如表 4-1 所示
字段类型 | 说明 |
StringField | 文本字段 |
TextAreaField | 多行文本字段 |
PasswordField | 密码文本字段 |
HiddenField | 隐藏文本字段 |
Datefield | 文本字段,值为 datetime.date 格式 |
DateTimeField | 文本字段,值为 datetime.datetime 格式 |
IntegerField | 文本字段,值为整数 |
DecimalField | 文本字段,值为 decimal.Decimal |
FloatField | 文本字段,值为浮点数 |
BooleanField | 复选框,值为True或False |
RadioField | 一组单选框 |
SelectField | 下拉列表 |
SelectMultipleField | 下拉列表,可选多个值 |
FileField | 文件上传字段 |
SubmitField | 表单提交按钮 |
FormField | 把表单作为字段嵌入另一个表单 |
FieldList | 一组指定类型的字段 |
验证函数 | 说明 |
验证电子邮件地址 | |
EqualTo | 比较两个字段的值:常用于要求输入两次密码进行确认的情况 |
IPAddress | 验证IPv4网络地址 |
Length | 验证输入字符串的长度 |
NumberRange | 验证输入的值在数字范围内 |
Optional | 无输入时跳过其他验证函数 |
Required | 确保字段中有数据 |
Regexp | 使用正则表达式验证输入值 |
URL | 验证URL |
AnyOf | 确保输入值在可选值列表中 |
NoneOf | 确保输入值不在可选值列表中 |
4.3 把表单渲染成HTML
表单字段是可调用的,在模板中调用后会渲染成HTML。
假设视图函数把一个NameForm实例通过参数form 传入模板,在模板中可以生成一个简单的表单,如下所示:
<form method="POST"> {{ form.hidden_tag() }} {{ form.name.label }} {{ form.name() }} {{ form.submit() }} </form>
当然,这个表单还很简陋。
要想改进表单的外观,可以把参数传入渲染字段的函数,传入的参数会被转换成字段的HTML属性。
例如,可以为字段指定 id 或 class 属性,然后定义 CSS 样式:
<form method="POST"> {{ form.hidden_tag() }} {{ form.name.label }} {{ form.name(id='my-text-field') }} {{ form.submit() }} </form>
即便能指定HTML属性,但按照这种方式渲染表单的工作量还是很大,所以在条件允许的情况下最好能使用Bootstrap中的表单样式。
Flask-Bootstrap 提供了一个非常高端的辅助函数,可以使用Bootstrap 中预先定义好的表单样式渲染整个Flask-WTF表单,而这些操作只需要一次调用即可完成。
使用Flask-Bootstrap ,上述表单可使用下面的方式渲染:
{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}
import 指令的使用方法和普通Python代码一样,允许导入模板中的元素并用在多个模板中。
导入的bootstrap/wtf.html 文件中定义了一个使用 Bootstrap 渲染 Flask-WTF表单对象的辅助函数。
wtf.quick_form() 函数的参数为Flask-WTF表单对象,使用Bootstrap的默认样式渲染传入的表单。
hello.py 的完整模板如示例4-3所示
示例 4-3 templates/index.html :使用Flask-WTF 和 Flask-Bootstrap渲染表单
{% extends "base.html" %} {% import "bootstrap/wtf.html" as wtf %} {% block page_content %} <div class="page-header"> <h1>Hello,{% if name %}{{ name }}{% else %}Stranger{% endif %}!</h1> </div> {{ wtf.quick_form(form) }} {% endblock %}
模板的内容区现在有两部分。
第一部分是页面头部,显示欢迎消息。
这里用到了一个模板条件语句。
Jinja2中的条件语句格式为 {% if condition %}...{% else %}...{% endif %}
如果条件的计算结果为True,则渲染 if 和 else 指令之间的值。
如果条件的计算结果为False,则渲染 else 和 endif 指令之间的值。
在这个例子中,如果没有定义模板变量 name,则会渲染字符串 "Hello Stranger!"。
内容区的第二部分使用 wtf.quick_form() 函数渲染 NameForm对象。
4.4 在视图函数中处理表单
在新版的hello.py中,视图函数 index()不仅要渲染表单,还要接收表单中的数据。
示例 4-4 是更新后的 index() 视图函数。
@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 参数 告诉Flask 在URL映射中把这个视图函数注册为 GET 和 POST 请求的处理程序。
如果没有指定 methods 参数,就只把视图函数注册为GEt请求的处理程序。
把POST加入方法列表很有必要,因为将提交表单作为POST请求进程处理更加便利。
表单也可作为GET请求提交,不过GET请求没有主体,提交的数据以查询字符串的形式附加到URL中,可在浏览器的地址栏中看到。
基于这个以及其他多个原因,提交表单大都作为POST请求进行处理。
局部变量 name 用来存放表单中输入的有效名字,如果没有输入,其值为None。
如上述代码所示,在视图函数中创建了一个NameForm类实例哟png与表示表单。
提交表单后,如果数据能被所有验证函数接受,那么 validdata_on_submit() 方法的返回值为 True,否则返回 Flase。
这个函数的返回值决定了是重新渲染表单还是处理表单提交的数据。
用户第一次访问程序时,服务器会收到一个没有表单数据的GET请求,所有 validate_on_submit() 将返回 False。
if 语句的内容将被跳过,通过渲染模板处理请求,并传入表单对象和值为None的name变量作为参数。用户会看到浏览器显示了一个表单。
用户提交表单后,服务器收到一个包含数据的POST请求。
validate_on_submit() 会调用name 字段上附属的 Required() 验证函数。
如果名字不为空,就通过验证,validate_on_submit() 返回 True。
现在,用户数据的名字可通过字段的data属性获取。
在 if 语句中,把名字赋值给局部变量 name ,然后再把data 属性设为 空字符串,从而清空表单字段。
最后一行调用 render_template() 函数渲染模板,但这一次参数 name的值为表单中数据的名字,因此会显示一个正对于该用户的欢迎消息。
用户首次访问网站时浏览器显示的表单。
用户提交名字后,程序会生成一个针对该用户的欢迎消息。
欢迎消息下方还是会显示这个表单,以便用户输入新名字。
如果用户提交表单之前没有输入名字,Required() 验证函数会捕获这个错误。
注意一下扩展自动提供了多少功能。
这个说明像 Flask-WTF 和 Flask-Bootstrap 这样设计;良好的扩展能让程序具有强大的功能。
4.5 重定向和用户会话
最新版的 hello.py 存在一个可用性问题。用户输入名字后提交表单,然后点击浏览器刷新按钮,会看到一个莫名其妙的警告,要求在再次提交表单之前进行确认。
之所以出现这种情况,是因为刷新页面时浏览器会重新发送之前已经发送过的最后一个请求。
如果这个请求是一个包含表单数据的POST请求,刷新页面后再次提交表单。
大多数情况下,这并不是理想的处理方式。
很多用户都不理解浏览器发出的这个警告。基于这个原因,最好别让Web程序把POST请求作为浏览器发送的最后一个请求。
这种需求的是实现方式是,使用 重定向 作为PSOT请求的响应,而不是使用常规响应。
重定向是一种特殊的响应,响应内容是URL,而不是包含HTML代码的字符串。
浏览器收到这种响应时,会向重定向的URL发起GET请求,显示页面的内容。
这个页面的加载可能要多花几微秒,因为要先发第二个请求发送给服务器。
除此之外,用户不会察觉到有身背不同。
现在,最有一个请求请是GET请求,所以刷新命令能像预期的那样正常使用了。
这个技巧称为 Post/重定向/Get 模式。
但这种方法会带来另一问题。程序处理POST请求时,使用 form.name.data 获取用户输入的名字,可是一旦这个请求结束,数据也就丢失了。
因为这个POST请求使用重定向处理,所以程序需要保存输入的名字,这样重定向后的请求才能获得并使用这个名字,从而构建真正的响应。
程序可以把数据存储在 用户会话 中,在请求之间 “记住” 数据。
用户会话是一种存有存储,存在于每个连接到服务器的客户端中。
我们在第2章介绍过用户会话,它是请求上下文中的变量,名为 session。就像标准的Python字典一样操作。
默认情况下,用户会话保存在客户点 cookie 中,使用设置的 SECRET_KEY 进行加密签名。
如果篡改了cookie 中的内容,签名就会失效,会话也会随之失效。
示例 4-5 的 index() 视图函数的新版本,实现了重定向和用户会话。
示例 4-5 hello.py :重定向和用户会话
from flask import Flask,render_template,session,redirect,url_for @app.route("/",methods=['GET','POST']) def index(): name = None form = NameForm() if form.validate_on_submit(): session['name'] = form.name.data return redirect(url_for('index')) return render_template('index.html',form=form,name=session.get('name'))
在程序的前一个版本中,局部变量 name 被用户存储用户在表单中输入的名字。
这个变量现在保存在用户会话中,即 session['name'],所以在两次请求之前也能记住输入的值。
现在,包含合法表单数据的请求最后会调用redirect()函数。
redirect()是个辅助函数,用来生成HTTP重定向响应。
redirect() 函数的参数是重定向的URL,这里使用的重定向URL是程序的根地址,因此重定向响应本可以简单一些,写成 redirect('/'),但却会使用Flask提供的URL生成函数url_for()。
推荐使用 url_for() 生成URL,因为这个函数使用URL映射成URL,从而保证URL和定义的路由兼容,而且修改路由名字后依然可用。
url_for() 函数的第一个且唯一必须制定的参数是端点名,即路由的内部名字。
默认情况下,路由的端点是相应视图函数的名字。
在这个示例中,处理跟地址的视图函数是index() ,因此传给url_for()函数的名字是 index。
最后一处改动位于 render_template() 函数中,使用session.get('name') 直接从会话中读取name参数的值。
和普通的字典一样,这里使用get()获取字典中键对应的值以避免未找到键的异常情况,因为对于不存在的键,get() 会返回默认值 None。
使用这个版本的程序时,刷新浏览器页面,你看到的新页面就和预期的一样了。
4.6 Flash 消息