一个网站通常是要接收用户输入的内容的,要实现这个功能,我们使用 Web 表单。在 Flask 中我们使用 Flask-WTF
插件来帮助实现这一功能。
Flask-WTF
是对 Python 第三方库 WTForms
进行了浅层次的封装并和 Flask 相结合的一个库。
安装
使用 pip
来安装,记住,安装前要激活虚拟环境:
(venv) $ pip install flask-wtf
这是本教程中第一次使用 pip
安装第三方库,安装的位置是在本地的虚拟环境中,如果我们的网站应用以后需要迁移或部署到服务器中,那么这些第三方库该怎么办呢?
我们在根目录使用 pip
的 freeze
命令来导出所有第三方库的版本号:
pip freeze > requirements.txt
现在根目录导出了一个 requirements.txt
文件,记录了现在所安装的库:
click==8.0.1
colorama==0.4.4
Flask==2.0.1
Flask-WTF==0.15.1
itsdangerous==2.0.1
Jinja2==3.0.1
MarkupSafe==2.0.1
Werkzeug==2.0.1
WTForms==2.3.3
将来需要在其他地方使用该网站应用的时候,直接 pip
读取该文件来安装对应的第三方库:
pip install -r requirements.txt
今后安装其他第三方库时,也请记住这个操作。
配置
教程进行到这一步,我们需要考虑网站配置的问题。最基本的解决方案是使用 app.config
对象,它是一个类似字典的对象。
因为我们使用了工厂函数,配置应当在 app/__init__.py
中的 create_app()
函数内编写:
# app/__init__.py
from flask import Flask
def create_app():
app = Flask(__name__)
# 配置
app.config['SECRET_KEY'] = 'you-will-never-guess'
# 引入蓝图并注册
from app.routes import main_routes
app.register_blueprint(main_routes)
return app
上面的代码虽然可以为应用创建配置,但是更佳的实践是使用松耦合。我们在根目录创建一个 config.py
文件来统一放置配置的内容。
# config.py
import os
class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
SECRET_KEY
暂时是这里添加的唯一配置选项,对 Flask
应用来说,它都是极其重要的。Flask
及其一些扩展使用密钥的值作为加密密钥,用于生成签名或令牌。Flask-WTF
插件使用它来保护网页表单免受 Cross-Site Request Forgery
或 CSRF
攻击。
密钥被定义成由 or
运算符连接两个项的表达式。第一个项查找环境变量 SECRET_KEY
的值,第二个项是一个硬编码的字符串。这种首先检查环境变量中是否存在这个配置,找不到的情况下就使用硬编码字符串。
在开发阶段,安全性要求较低,因此可以直接使用硬编码字符串。但是,当应用部署到生产服务器上的时候,我将设置一个独一无二且难以揣摩的环境变量,这样,服务器就拥有了一个别人未知的安全密钥了。
windows 下设置环境变量:
set SECRET_KEY=your-secret-key
如果在 Linux 服务器上,使用 export
命令来设置环境变量。
编写好配置文件,我还需要让 Flask 读取并使用它,在 Flask app 生成后,利用app.config.from_object()
方法来完成这个操作:
# # app/__init__.py
from flask import Flask
from config import Config
def create_app():
app = Flask(__name__)
# 加载配置
app.config.from_object(Config)
# 引入蓝图并注册
from app.routes import main_routes
app.register_blueprint(main_routes)
return app
我们可以使用字典语法来读取 config
文件中的配置值:
>>> from microblog import app
>>> app.config['SECRET_KEY']
'you-will-never-guess'
用户登录表单
Flask-WTF
插件使用 Python 类来定义 Web 表单。表单类只需将表单的字段定义为类属性即可。
为了践行松耦合原则,表单类代码会单独编写在 app/forms.py
文件中。
# 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')
现在我们就定义好了一个用户登录表单,它要求用户输入 username
和 password
,并提供一个 remember me
的复选框和提交按钮。
由于 Flask-WTF
插件本身不提供字段类型,因此直接从 WTForms
包中导入了四个表示表单字段的类。每个字段类都接受一个描述或别名作为第一个参数,并生成一个实例来作为 LoginForm
的类属性。
可选参数 validators
用于验证输入字段是否符合预期。DataRequired
验证器验证字段输入是否为空。
表单模板
下一步是将表单添加到 HTML 模板以便渲染到网页上。 因为 LoginForm
类中定义的字段支持自渲染为 HTML 元素,所以这个任务相当简单。
我将把登录模板代码编写在 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
来继承 base.html
基础模板。事实上,我将会对所有的模板继承基础模板,以保持顶部导航栏风格统一。
这个模板需要一个 form
参数的传入到渲染模板的函数中,form
来自于 LoginForm
类的实例化,等下我们会在对应的视图函数中编写它。
HTML 的 <form>
元素被用作 Web 表单的容器。
表单的 action
属性告诉浏览器在提交表单时应该请求的 URL。 当 action
设置为空字符串时,表单将被提交给当前地址栏中的 URL,即当前页面。
method
属性指定了将表单提交给服务器时应该使用的 HTTP 请求方法。 默认情况下是用 GET
请求发送,但作为登录表单使用 POST
请求更为合适。
form.hidden_tag()
模板参数生成了一个隐藏字段,其中包含一个用于保护表单免受 CSRF 攻击的 token
。 对于保护表单,你需要做的所有事情就是在模板中包括这个隐藏的字段,并在 Flask 配置中定义 SECRET_KEY
变量,Flask-WTF
会完成剩下的工作。
如果你以前编写过 HTML Web 表单,那么你会发现一个奇怪的现象——在此模板中没有 HTML 表单元素,这是因为表单的字段对象会在渲染时会自动转化为 HTML 元素。
比如 {{ form.username.label }}
就好被渲染为:<label for="username">Username</label>
;{{ form.username(size=32) }}
就被渲染为对应的 input
输入框。
表单视图
编写好 HTML 模板文件后,需要把模板和对应的视图函数结合起来,函数的逻辑只需创建一个 form
实例,并将其传入渲染模板的函数中即可,然后用 /login
URL 来关联它。
同样地,该视图函数编写在 app/routes.py
模块中:
# app/routes.py
from flask import render_template, Blueprint
from app.forms import LoginForm
main_routes = Blueprint('main', __name__)
# ...
@main_routes.route('/login')
def login():
form = LoginForm()
return render_template('login.html', title='Sign In', form=form)
在基础模板 templates/base.html
的导航栏上添加链接,以便访问:
# templates/base.html
<div>
Microblog:
<a href="/index">Home</a>
<a href="/login">Login</a>
</div>
现在登陆页面就已经完成了:
接收表单数据
现在点击提交按钮,浏览器将显示 “Method Not Allowed” 错误。为什么呢? 这是因为之前的登录视图功能到目前为止只完成了一半的工作。 它可以在网页上显示表单,但没有逻辑来处理用户提交的数据。
接下来我们完成接收表单数据这部分工作,改写视图函数 login()
:
# app/routes.py
from flask import (
render_template,
Blueprint,
flash,
redirect
)
from app.forms import LoginForm
# ...
@main_routes.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)
路由装饰器中的 methods
参数,定义了这个视图函数接受 GET
和 POST
请求。之前的错误 “Method Not Allowed” 正是由于视图函数还未配置允许 POST
请求。
form.validate_on_submit()
实例方法会执行 form
校验的工作。当浏览器发起 GET
请求的时候,它返回 False
,这样视图函数就会跳过 if
块中的代码,直接转到视图函数的最后一句来渲染模板。
当用户在浏览器点击提交按钮后,浏览器会发送 POST
请求。 form.validate_on_submit()
就会获取到所有的数据,运行字段各自的验证器,全部通过之后就会返回 True
,这表示数据有效。
一旦有任意一个字段未通过验证,这个实例方法就会返回 False
,引发类似 GET
请求那样的表单的渲染并返回给用户。稍后我会在添加代码以实现在验证失败的时候显示一条错误消息。
当 form.validate_on_submit()
返回 True
时,登录视图函数调用从 Flask导入的两个新函数。 flash()
函数可以在新加载的页面中插入新的消息。redirect()
函数指引浏览器重定向到它的参数所关联的 URL。
为了使得 flash()
函数起作用,我们需要修改基础模板文件 templates/base.html
:
# templates/base.html
<html>
<head>
{% if title %}
<title>{{ title }} - Microblog</title>
{% else %}
<title>Welcome to 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>
get_flashed_messages()
函数返回用 flash()
注册过的消息列表。此处使用 with
语句将 get_flashed_messages()
的结果赋值给变量 messages
。接下来的代码就是检查 messages
变量是否包含消息,如果有,则用一个循环语句把它们渲染出来。
消息闪现有一个特性,一旦通过 get_flashed_messages()
函数请求了一次,它们就会从消息列表中移除,所以在调用 flash()
函数后它们只会出现一次。
现在,我们的消息闪现系统开始工作了:
完善字段验证
表单字段的验证器可防止无效数据被接收到应用中。一个对用户友好的表单应当是在用户输入无效或错误信息的时候,出现提示信息以便用户更正输入。下面我们使用验证器来实现这一功能。
# 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) }}
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label }}
<br>
{{ form.password(size=32) }}
{% 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 %}
如果我们不输入任何信息直接点击提交按钮,就会看到这样的提示消息:
这些错误提示信息是哪里来的呢?实际上,表单验证器自带了这些描述性错误消息,它们储存在 form.<field_name>.errors
变量中,在对应的错误出现时被激发。
一个字段的验证错误信息结果是一个列表,因为字段可以附加多个验证器,并且多个验证器都可能会提供错误消息以显示给用户。
我们也可以自定义任何字段验证器和错误提示消息,具体操作参见: 官方文档。
生成链接
现在的登录表单已经相当完整了,在本章中,我们新增了一些路由 URL,它们现在都是用硬编码的方式直接写到代码里的,比如:
<div>
Microblog:
<a href="/index">Home</a>
<a href="/login">Login</a>
</div>
再比如:
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
# ...
return redirect('/index')
# ...
现在这些链接都是能正常工作的,但一旦有一天我们需要对 URL 作重新调整,手动更改这些硬编码将会是一个噩梦。
为了更好地管理这些链接,Flask 提供了一个名为 url_for()
的函数,它使用 URL 到视图函数的内部映射关系来生成 URL。
例如,url_for('login')
返回 /login
,url_for('index')
返回 /index
。url_for()
的参数是 endpoint
名称,也就是视图函数的名字。当然我们先在的路由使用了蓝图,使用 url_for()
时候还要加上蓝图的名字。
现在我们用 url_for()
重新组织 URL:
# app/templates/base.html
<div>
Microblog:
<a href="{{ url_for('main.index') }}">Home</a>
<a href="{{ url_for('main.login') }}">Login</a>
</div>
login()
视图函数里也要变更:
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('main.index'))
# ...
本篇源码:https://github.com/SingleDiego/Flask-Tutorial-Source-Code/tree/SingleDiego-patch-03
网友评论