如何使用Web表单

我为应用主页创建了一个简单的模板,并使用诸如用户和用户动态的模拟对象。在本章中,我将解决这个应用程序中仍然存在的众多遗漏之一,那就是如何通过Web表单接受用户的输入。

Web表单是所有Web应用程序中最基本的组成部分之一。 我将使用表单来为用户发表动态和登录认证提供途径。

Flask-WTF简介

我们使用Flask-WTF插件来处理本应用中的Web表单,它对WTForms进行了浅层次的封装以便和Flask完美结合。这是本应用引入的第一个Flask插件。插件是Flask生态中的举足轻重的一部分,Flask故意设计为只包含核心功能以保持代码的整洁,并暴露接口以对接解决不同问题的插件。

Flask插件都是常规的Python三方包,可以使用pip安装。

pip install flask-wtf

配置

到目前为止,这个应用程序都非常简单,因此我不需要考虑它的配置。 但是,除了最简单的应用,你会发现Flask(也可能是Flask插件)为使用者提供了一些可自由配置的选项。你需要决定传入什么样的配置变量列表到框架中。

有几种途径来为应用指定配置选项。最基本的解决方案是使用app.config对象,它是一个类似字典的对象,可以将配置以键值的方式存储其中。例如,你可以这样做:

app = Flask(__name__)
app.config['SECRET_KEY'] = 'you-will-never-guess'
# ... add more variables here as needed

上面的代码虽然可以为应用创建配置,但是我有松耦合的癖好。因此,我不会让配置和应用代码处于同一个部分,而是使用稍微复杂点的结构,将配置保存到一个单独的文件中。

使用类来存储配置变量,才是我真正的风格。我会将这个配置类存储到单独的Python模块,以保持良好的组织结构。下面就让你见识一下这个存储在顶级目录下,名为config.py的模块的配置类吧:

import osclass Config(object):SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'

简单的不像话,有没有? 配置设置被定义为Config类中的属性。 一旦应用程序需要更多配置选项,直接依样画葫芦,附加到这个类上即可,稍后如果我发现需要多个配置集,则可以创建它的子类。现在则不用操心。

SECRET_KEY是我添加的唯一配置选项,对大多数Flask应用来说,它都是极其重要的。Flask及其一些扩展使用密钥的值作为加密密钥,用于生成签名或令牌。Flask-WTF插件使用它来保护网页表单免受名为Cross-Site Request Forgery或CSRF的恶意攻击。顾名思义,密钥应该是隐密的,因为由它产生的令牌和签名的加密强度保证,取决于除了可信维护者之外,没有任何人能够获得它。

密钥被定义成由or运算符连接两个项的表达式。第一个项查找环境变量SECRET_KEY的值,第二个项是一个硬编码的字符串。这种首先检查环境变量中是否存在这个配置,找不到的情况下就使用硬编码字符串的配置变量的模式你将会反复看到。在开发阶段,安全性要求较低,因此可以直接使用硬编码字符串。但是,当应用部署到生产服务器上的时候,我将设置一个独一无二且难以揣摩的环境变量,这样,服务器就拥有了一个别人未知的安全密钥了。

拥有了这样一份配置文件,我还需要通知Flask读取并使用它。可以在生成Flask应用之后,利用app.config.from_object()方法来完成这个操作:

from flask import Flask
from config import Configapp = Flask(__name__)
app.config.from_object(Config)from app import routes

导入Config类的方式,乍一看可能会让人感到困惑,不过如果你注意到从flask包导入Flask类的过程,就会发现这其实是类似的操作。 显而易见,小写的“config”是Python模块config.py的名字,另一个含有大写“C”的是类。

正如我上面提到的,可以使用app.config中的字典语法来访问配置项。 在下面的Python交互式会话中,你可以看到密钥的值:

>>> from microblog import app
>>> app.config['SECRET_KEY']
'you-will-never-guess'

用户登录表单

Flask-WTF插件使用Python类来表示Web表单。表单类只需将表单的字段定义为类属性即可。

将表单类单独存储到名为app/forms.py的模块中。就让我们来定义用户登录表单来做一个开始吧,它会要求用户输入username和password,并提供一个“remember me”的复选框和提交按钮:

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequiredclass LoginForm(FlaskForm):username = StringField('Username', validators=[DataRequired()])password = PasswordField('Password', validators=[DataRequired()])remember_me = BooleanField('Remember Me')submit = SubmitField('Sign In')

大多数Flask插件使用flask_ <name>命名约定来导入,Flask-WTF的所有内容都在flask_wtf包中。在本例中,app/forms.py模块的顶部从flask_wtf导入了名为FlaskForm的基类。

由于Flask-WTF插件本身不提供字段类型,因此我直接从WTForms包中导入了四个表示表单字段的类。每个字段类都接受一个描述或别名作为第一个参数,并生成一个实例来作为LoginForm的类属性。

你在一些字段中看到的可选参数validators用于验证输入字段是否符合预期。DataRequired验证器仅验证字段输入是否为空。更多的验证器将会在未来的表单中接触到。

表单模板

下一步是将表单添加到HTML模板以便渲染到网页上。 令人高兴的是在LoginForm类中定义的字段支持自渲染为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 %}

这个模板需要一个form参数的传入到渲染模板的函数中,form来自于LoginForm类的实例化,不过我现在还没有编写它。

HTML<form>元素被用作Web表单的容器。 表单的action属性告诉浏览器在提交用户在表单中输入的信息时应该请求的URL。 当action设置为空字符串时,表单将被提交给当前地址栏中的URL,即当前页面。 method属性指定了将表单提交给服务器时应该使用的HTTP请求方法。 默认情况下是用GET请求发送,但几乎在所有情况下,使用POST请求会提供更好的用户体验,因为这种类型的请求可以在请求的主体中提交表单数据, GET请求将表单字段添加到URL,会使浏览器地址栏变得混乱。

form.hidden_tag()模板参数生成了一个隐藏字段,其中包含一个用于保护表单免受CSRF攻击的token。 对于保护表单,你需要做的所有事情就是在模板中包括这个隐藏的字段,并在Flask配置中定义SECRET_KEY变量,Flask-WTF会完成剩下的工作。

如果你以前编写过HTML Web表单,那么你会发现一个奇怪的现象——在此模板中没有HTML表单元素,这是因为表单的字段对象的在渲染时会自动转化为HTML元素。 我只需在需要字段标签的地方加上{{ form.<field_name>.label }},需要这个字段的地方加上{{ form.<field_name>() }}。 对于需要附加HTML属性的字段,可以作为关键字参数传递到函数中。 此模板中的username和password字段将size作为参数,将其作为属性添加到<input> HTML元素中。 你也可以通过这种手段为表单字段设置class和id属性。

表单视图

完成这个表单的最后一步就是编写一个新的视图函数来渲染上面创建的模板。

函数的逻辑只需创建一个form实例,并将其传入渲染模板的函数中即可,然后用*/login* URL来关联它。这个视图函数也存储到app/routes.py模块中,代码如下:

from flask import render_template
from app import app
from app.forms import LoginForm# ...@app.route('/login')
def login():form = LoginForm()return render_template('login.html', title='Sign In', form=form)

我从forms.py导入LoginForm类,并生成了一个实例传入模板。form=form的语法看起来奇怪,这是Python函数或方法传入关键字参数的方式,左边的form代表在模板中引用的变量名称,右边则是传入的form实例。这就是获取表单字段渲染结果的所有代码了。

在基础模板templates/base.html的导航栏上添加登录的链接,以便访问:

<div>Microblog:<a href="/index">Home</a><a href="/login">Login</a>
</div>

此时,你可以验证结果了。运行该应用,在浏览器的地址栏中输入http://localhost:5000/,然后点击顶部导航栏中的“Login”链接来查看新的登录表单。

接收表单数据

点击提交按钮,浏览器将显示“Method Not Allowed”错误。为什么呢? 这是因为之前的登录视图功能到目前为止只完成了一半的工作。 它可以在网页上显示表单,但没有逻辑来处理用户提交的数据。Flask-WTF可以轻松完成这部分工作, 以下是视图函数的更新版本,它接受和验证用户提交的数据:

from flask import render_template, flash, redirect@app.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参数。 它告诉Flask这个视图函数接受GETPOST请求,并覆盖了默认的GET。 HTTP协议规定对GET请求需要返回信息给客户端(本例中是浏览器)。 本应用的所有GET请求都是如此。 当浏览器向服务器提交表单数据时,通常会使用POST请求(实际上用GET请求也可以,但这不是推荐的做法)。之前的“Method Not Allowed”错误正是由于视图函数还未配置允许POST请求。 通过传入methods参数,你就能告诉Flask哪些请求方法可以被接受。

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()函数后,Flask会存储这个消息,但是却不会奇迹般地直接出现在页面上。模板需要将消息渲染到基础模板中,才能让所有派生出来的模板都能显示出来。更新后的基础模板代码如下:

<html><head>{% if title %}<title>{{ title }} - microblog</title>{% else %}<title>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>

此处我用了with结构在当前模板的上下文中来将get_flashed_messages()的结果赋值给变量messagesget_flashed_messages()是Flask中的一个函数,它返回用flash()注册过的消息列表。接下来的条件结构用来检查变量messages是否包含元素,如果有,则在<ul>元素中,为每条消息用<li>元素来包裹渲染。这种渲染的样式结果看起来不会美观,之后会有主题讲到Web应用的样式。

闪现消息的一个有趣的属性是,一旦通过get_flashed_messages函数请求了一次,它们就会从消息列表中移除,所以在调用flash()函数后它们只会出现一次。

时机成熟,再次测试表单吧,将username和password字段留空并点击提交按钮来观察DataRequired验证器是如何中断提交处理流程的。

完善字段验证

表单字段的验证器可防止无效数据被接收到应用中。 应用处理无效表单输入的方式是重新显示表单,以便用户进行更正。

如果你尝试过提交无效的数据,相信你会注意到,虽然验证机制查无遗漏,却没有给出表单错误的具体线索。下一个任务是通过在验证失败的每个字段旁边添加有意义的错误消息来改善用户体验。

实际上,表单验证器已经生成了这些描述性错误消息,所缺少的不过是模板中的一些额外的逻辑来渲染它们。

这是给username和password字段添加了验证描述性错误消息渲染逻辑之后的登录模板:

{% 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) }}<br>{% for error in form.username.errors %}<span style="color: red;">[{{ error }}]</span>{% endfor %}</p><p>{{ form.password.label }}<br>{{ form.password(size=32) }}<br>{% 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 %}

我做的唯一的改变是,在username和password字段之后添加for循环以便用红色字体来渲染验证器添加的错误信息。通常情况下,拥有验证器的字段都会用form.<field_name>.errors来渲染错误信息。 一个字段的验证错误信息结果是一个列表,因为字段可以附加多个验证器,并且多个验证器都可能会提供错误消息以显示给用户。

如果你尝试在未填写username和password字段的情况下提交表单,就可以看到显眼的红色错误信息了。

生成链接

现在的登录表单已经相当完整了,但在结束本章之前,我想讨论在模板和重定向中包含链接的妥当方法。 到目前为止,你已经看到了一些定义链接的例子。 例如,这是当前基础模板中的导航栏代码:

    <div>Microblog:<a href="/index">Home</a><a href="/login">Login</a></div>

登录视图函数同样定义了一个传入到redirect()函数作为参数的链接:

@app.route('/login', methods=['GET', 'POST'])
def login():form = LoginForm()if form.validate_on_submit():# ...return redirect('/index')# ...

直接在模板和源文件中硬编码链接存在隐患,如果有一天你决定重新组织链接,那么你将不得不在整个应用中搜索并替换这些链接。

为了更好地管理这些链接,Flask提供了一个名为url_for()的函数,它使用URL到视图函数的内部映射关系来生成URL。 例如,url_for('login')返回/loginurl_for('index')返回/indexurl_for()的参数是endpoint名称,也就是视图函数的名字。

你可能会问,为什么使用函数名称而不是URL? 事实是,URL比起视图函数名称变更的可能性更高。 稍后你会了解到的第二个原因是,一些URL中包含动态组件,手动生成这些URL需要连接多个元素,枯燥乏味且容易出错。 url_for()生成这种复杂的URL就方便许多。

因此,从现在起,一旦我需要生成应用链接,我就会使用url_for()。基础模板中的导航栏部分代码变更如下:

    <div>Microblog:<a href="{{ url_for('index') }}">Home</a><a href="{{ url_for('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('index'))# ...

Flask学习系列(三)相关推荐

  1. 数据库MYSQL学习系列三

    数据库MYSQL学习系列三 三.MYSQL事务与存储引擎 3.1-数据库事务 什么是事务 一系列有序的数据库操作: o要么全部成功 o要么全部回退到操作前的状态 o中间状态对其他连接不可见 事务的 ...

  2. Pyside2 学习系列三:PyInstaller打包项目瘦身(超详细的Pyside2 攻略)

    继上一篇文章打包后,发现包太大了,我们现在开始进行瘦身优化. 本项目的所有演示代码:github可在这里下载. 打包只用的工具为PyInstaller. 不想细看的朋友 可以直接跳到 最后 方法对比 ...

  3. Vue+Electron学习系列 (三) -- 自动更新

    Vue+Electron学习系列 ​​​​​​​1️⃣Vue+Electron学习系列 (一) -- 初识​​​​​​​ 2️⃣ Vue+Electron学习系列 (二) -- 打包发布 3️⃣ Vu ...

  4. idea学习系列三之版本管理工具

    idea学习系列三之版本管理工具 上一篇文章主要介绍了将idea中常用的快捷键,快捷键要想熟练那就得在实际的工作中多使用,刚开始可能很麻烦经常去笔记中找,不过用多了自然就熟练了. 这篇文件将会介绍一下 ...

  5. Java NIO学习系列三:Selector

    前面的两篇文章中总结了Java NIO中的两大基础组件Buffer和Channel的相关知识点,在NIO中都是通过Channel和Buffer的协作来读写数据的,在这个基础上通过selector来协调 ...

  6. NMock学习系列(三)--- NMock在DDD领域驱动的单元测试中的应用

    介绍 领域驱动设计涵盖的知识点比较多,其中代码的架构.设计.编写基本上只占到其中的很小一部分,其它的大部分讲解的是需求的获取方式.项目的管理方式等知识.本篇就是针对这一小部分的知识点位来展开的.所以本 ...

  7. Cosmos的基石:IL2CPU编译器--.net/C#开源操作系统学习系列三

    本文的代码包以cosmos-12304.zip为例(从这个包开始,COSMOS的内核算是有了个基本的雏形,就像是一颗大树在出芽前会先长出庞大的根系,现在就要破土长出第一颗芽了) IL2CPU之于COS ...

  8. Java I/O系统学习系列三:I/O流的典型使用方式

    尽管可以通过不同的方式组合IO流类,但我们可能也就只用到其中的几种组合.下面的例子可以作为典型的IO用法的基本参考.在这些示例中,异常处理都被简化为将异常传递给控制台,但是这只有在小型示例和工具中才适 ...

  9. RabbitMQ入门学习系列(三).消息发送接收

    快速阅读 用Rabitmq的队列管理,以及如何保证消息在队列中不丢失.通过ack的消息确认和持久化进行操作.以及Rabbit中如何用Web面板进行管理队列.消费者如何处理耗时的任务 生产者代码创建链接 ...

最新文章

  1. 占据栅格地图(Occupancy Grid Map)
  2. ID3、C4.5、CART树算法简介,这几个算法有什么区别?对于异常值和分类值有什么需要注意的?
  3. 【v2.x OGE教程 18】 Entity相关
  4. SpringBoot-Security 具体案例、 实现安全框架、权限控制、aop切入
  5. vs2005's addin folder
  6. 请描述定时器初值的计算方式_单片机C语言编程中定时器初值计算的两种方法...
  7. Android事件分发小结
  8. 如何限制浏览器使用_如何使用浪涌电流限制器NTC(一)
  9. IOS-项目中常见文件介绍
  10. java中异常抛出后代码还会继续执行吗
  11. 彼得定律与员工职业生涯规划(该提拔谁,职业规划,知人善用)
  12. vc sleep不占进程_Python 中的进程深入
  13. jQuery实现锚点滑动定位
  14. iphone编程,使用代码结束程序
  15. 微积分知识总览(1)——微积分准备知识
  16. 2021 泰迪杯 A 题思路
  17. 读万卷书,行万里路,写万行Code
  18. 秒懂三层交换机的作用及使用
  19. Storm-kafka【接口实现】4 - KafkaSpout
  20. volatile内存屏障

热门文章

  1. 个人用户永久免费,可自动升级版Excel插件,使用VSTO开发,Excel催化剂功能第9波-数据透视表自动设置...
  2. windows10一键安装linux,WSLg–微软官方内置,在Win 10上一键安装5大Linux发行版本
  3. 软件开发设计思想与6大软件设计原则、23种设计模式目录-设计模式
  4. oracle日期加减计算
  5. Hard Drive
  6. 错误代码512linux,linux下命令执行错误代码
  7. 几种简单卷积计算方法实例-Faltung
  8. 防洪防汛安全技术交底(通用模板)
  9. 「法外狂徒」ChatGPT!30年老律师用它旁征博引,结果被骗得禁止执业
  10. 牛逼,这个 PDF 库比其它工具好用 99 倍