美文网首页
03.Web 表单

03.Web 表单

作者: SingleDiego | 来源:发表于2021-06-28 10:34 被阅读0次

一个网站通常是要接收用户输入的内容的,要实现这个功能,我们使用 Web 表单。在 Flask 中我们使用 Flask-WTF 插件来帮助实现这一功能。

Flask-WTF 是对 Python 第三方库 WTForms 进行了浅层次的封装并和 Flask 相结合的一个库。






安装

使用 pip 来安装,记住,安装前要激活虚拟环境:

(venv) $ pip install flask-wtf

这是本教程中第一次使用 pip 安装第三方库,安装的位置是在本地的虚拟环境中,如果我们的网站应用以后需要迁移或部署到服务器中,那么这些第三方库该怎么办呢?

我们在根目录使用 pipfreeze 命令来导出所有第三方库的版本号:

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 ForgeryCSRF 攻击。

密钥被定义成由 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')

现在我们就定义好了一个用户登录表单,它要求用户输入 usernamepassword,并提供一个 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 参数,定义了这个视图函数接受 GETPOST 请求。之前的错误 “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') 返回 /loginurl_for('index') 返回 /indexurl_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

相关文章

  • 03.Web 表单

    一个网站通常是要接收用户输入的内容的,要实现这个功能,我们使用 Web 表单。在 Flask 中我们使用 Flas...

  • bootstrap之form表单

    表单布局 垂直表单(默认) 内联表单 水平表单 垂直表单或基本表单(display:block;) 创建基本表单的...

  • 【读书笔记+思考】移动设备表单设计

    在移动界面中,常见的表单模式有:登录表单;注册表单;核对表单;计算表单;搜索表单;多步骤表单;长表单等 登录表单:...

  • bootstrap表单

    表单布局 垂直表单(默认) 内联表单 水平表单 垂直表单或基本表单 基本的表单结构是 Bootstrap 自带的,...

  • bootstrap 表单布局的三种方式

    三种 垂直表单(默认) 内联表单 水平表单 垂直表单 效果 内联表单 效果 水平表单 效果 参考:https://...

  • 表单相关总结

    表单?表单作用:收集用户信息。表单组成:表单域、表单控件、提示信息。 表单域常用属性 常用属性: name=...

  • 网页设计:HTML表单标签

    表单包含三个基本组成部分:表单标签、表单域、表单按钮。 1,表单标签 HTML 表单用于收集用户输入,表单使用 ...

  • 2019-04-09 表单(5)

    表单布局Bootstrap 提供了下列类型的表单布局: 垂直表单(默认) 内联表单 水平表单 1.垂直或基本表单 ...

  • 动态表单实现

    angular动态表单 地址: angular表单 vue动态表单 地址: vue表单

  • bootstrap表单

    垂直表单(默认) 内联表单 水平表单 垂直表单 也称基本表单基本的表单结构是 bootstrap 自带的创建基本表...

网友评论

      本文标题:03.Web 表单

      本文链接:https://www.haomeiwen.com/subject/doynyltx.html