Flask学习笔记(未完结)

准备Flask环境

  • 具备python环境
  • 安装virtualenv
yum install python-virtualenv  //centos 安装
apt-install python-virtualenv  //debian 安装
easy_install virtualenv  //easy_install 安装
  • 构建python虚拟环境
virtualenv  [-p /usr/bin/python3 ] env   //指定env为python环境路径; -p 指定python路径
source  ./env/bin/activate    //激活环境
deactivate  //关闭环境
  • 安装flask pip3 install flask

Flask基本结构

Hello World

from flask import Flask
from flask import url_for

app = Flask(__name__)

 #使用装饰器指定'/'到函数'index'的路由
@app.route('/')   
def index():
    return '<h1>Hello World!</h1>'

#通过URL为函数传递参数
@app.route('/login/<username>/')
def login(username):
    return '<h1 style="color:blue;">Hello, %s</h1>' % username

#使用url_for()函数,通过视频函数名称获取URL
@app.route('/url_test')
def get_url():
    index_url = url_for('index')
    login_url = url_for('login', username='Mark')
    return f'<a href={index_url}> Home Page </a>' + '</br>' + f'<a href={login_url}>Login</a>'


if  __name__ == '__main__':
    app.run(host='0.0.0.0', debug=True)

#也可以在shell中使用flask run 命令, 该命令自动检测同目录下的flask代码文件, 并加载app对象

#flask routes  #可以查看URL和视图函数的映射关系

调试器与重载器

调试器

export FLASK_DEBUG=1  #开启调试器

重载器(reloader)

监视文件变化 , 自动加载新的代码.

Flask Shell

在程序文件目录执行flask shell, 会启动一个python shell, 并自动加载目录下的flask代码文件.

Flask 目录结构

hello/      #项目目录
    - templates/   #模板文件
    - static/       #静态文件, 存放CSS / JavaScript
    - app.py        #项目文件

请求-响应循环

Image from Flask Web

请求/应答上下文

Request对象

该对象封装了从客户端发来的请求报文, 我们可以从它获取请求报文的所有数据

from flask import request 导入访问请求对象(request) , request是一个线程中的全局可访问对象, 该对象包含了客户端请求报文(客户端浏览器发给web服务器的http请求包)中的所有内容.

from flask import request

@app.route('/browser')
def browser():
    #从http请求包中获取客户端浏览器名称
    return '<h1>Your browser is %s' % \          request.user_agent.browser   

#request.headers.get('User-Agent')  也可以获取agent信息
#####
#当前请求的方法, "GET", "POST", "PUT"...
request.method  
#请求连接中URL后边的参数, 如: www.abc.com/wx?p1=123&p2=456
request.args

flask.request 对象的属性参数使用Dash查阅.

设置HTTP监听方法

可以在@app.route()装饰器方法中指定监听的请求方法.

@app.route('/login/', methods=['GET', 'POST']
def login():
    pass

URL参数类型转换

@app.route('/goback/<int:year>')
def go_back(year):
  print(f'type of year is: {type(year)}')
  return '<p>Welcome to %d!</p>' % (2018 - year)

请求勾子

勾子使用装饰器实现

  • before_first_request:注册一个函数,在处理第一个请求之前运行。
    • 常用于做一个初始化操作, 如: 创建数据库表, 添加管理员用户,etc.
  • before_request:注册一个函数,在每次请求之前运行。
    • 常用于记录一些统计数据,如果用户在线时间.
  • after_request:注册一个函数,如果没有未处理的异常抛出,在每次请求之后运行。
    • 用于善后操作, 例如用户修改的数据, 最后提交更改到数据库
  • after_this_request: 完成一个视图函数请求后, 调用.
  • teardown_request:注册一个函数,即使有未处理的异常抛出,也在每次请求之后运行。
Image from Flask Web开发实战, page 84

例:

@app.before_first_request
def before_request():
  print('this message output before request')

响应

**方案一: 视图函数可以返回三个变量: **

  • http的应答报文的body, 类型为字符串. <h1>Hello world</h1>
  • 应答报文的状态码, 类型为整型. 200 (可选参数)
  • 应答报文header字段值,类型为字典. {'server':'XXX', 'Date':'Wed, 05 Dec 2018'} (可选参数)

示例:

@app.route('/')
def index():
    return '<h1>Hello World!</h1>',200,{'server':'markict'}

方案二: 通过返回response对象来返回body,状态代码,头部字段

make_response()函数可以接受1个,2个或3个参数(对应视图函数的返回值),并返回一个response对象. 我们也可以对response对象做进一步设置(如:设置cookie), 然后由视图函数进行返回.

from flask import Flask
from flask import request
from flask import make_response

@app.route('/browser')
def browser():
    #构建body的html, 通过request获取浏览器类型
    body = '<h1>Your browser is %s' % \
    request.user_agent.browser
    #设置状态代码
    state_code = 200
    #应答报文头部
    header = {'server':'Markict', 'Date':'Fri 05 Dec 2018'}
    #使用make_reponse()函数构建response对象
    response = make_response(body, state_code, header)
    #为response对象设置cookie
    response.set_cookie('answer', '42')
    #返回response
    return response

response对象常用的属性:

对象/方法名称 描述
response.headers HTTP响应报文首部
response.status_code HTTP响应报文状态代码
response.status HTTP响应报文状态, 文本表示
response.mimetype HTTP响应报文的内容类型
response.set_cookie() 用于为HTTP响应报文设置Cookie

响应-重定向

重定向使用状态代码 302, 和报文头部中的Location字段指定重定向的目标站点.

我们可以使用视图函数返回三个变量的形式, 也可以使用flask中的redirect()函数来简化操作.

from flask import redirect

@app.route('/')
def index():
    #返回三个变量,来指定重定向的代码和目标站点.
    #return 'hello', 302, \
    {'Location':'http://www.baidu.com'}
    #使用redirect()函数进行重定向.
    return redirect('http://www.baidu.com')

响应-错误

使用abort函数进行错误响应,把控制权交给browser.

from flask import abort

@app.route('/login/<user>')
def login(user):
    #如果用户名不是纯字母,那么进行404错误响应
    if not user.isalpha():
        abort(404)
    return '<h1>hello, %s</h1>' % user

响应格式

响应格式是指定返回给客户端(浏览器)的HTTP报文头部中标明的数据类型(MIME Type), 常用的有:

text/plain, text/html, application/xml, application/json

可以通过response对象的mimetype属性指定

from flask import Flask, make_response, json
@app.route('/foo')
def foo():
  data = {'name':'mark', 'gender':'male'}
  r = make_response(json.dumps(data))
  r.mimetype = 'application/json'
  return response

Cookie

HTTP是无状态(stateless)协议。也就是说,在一次请求响应结束 后,服务器不会留下任何关于对方状态的信息。为了解决这类问题,就有了 Cookie技术。Cookie技术通过在请求和响应报文中添加Cookie数据来保 存客户端的状态信息。

Cookie由Server端生成并维护, 发给client端, client浏览器会保留一段时间, 并在该时间内, 当向server发起请求时携带Cookie,以标明身份.

response.set_cookie()用于为响应对象设置Cookie.

set_cookie()方法参数:

属性 描述
key Cookie的键名
value Cookie值
max_age cookie保存时间(秒), 关闭浏览器也会过期
expires 具体过期时间, 一个datetime对象
domain 设置Cookie可用域
secure 如果设置为True,仅HTTPS可用
httponly 禁止客户端的javascript读取Cookie
from flask import Flask, make_response
...
@app.route('/set/<name>')
def set_cookie(name):
  response = make_response(redirect(url_for('hello')))
  response.set_cookie('name',name)
  return response

在Cookie过期之前, 客户端发给服务器的请求将携带Cookie.

Session

Cookie是明文传输,存在安全隐患, Session的方式是通过在server端预先设置一个secret_key, 然后把Cookie用这个key进行加密签名,并发送给client. client只可以使用该session, 并不能修改它的值.

from flask import Flask, session, request, make_response, redirect

@app.route('/login/<name>')
def login(name):
  app.secret_key = 'passwd123'
  session['name'] = 'cookie_value'  #设置Cookie
  response = make_respones(f'hello Mr.{name}')
  return response

@app.route('/greet')
def greet():
  if 'cookie_value' in session:  #验证Cookie
    return "<h1>Welcome</h1>"
  else:
    flask.abort()
    
@app.route('/logout')
def logout():
  if "cookie_value" in session:
    session.pop('name')   #登出就是把设置的Cookie从session弹出
  return redirect(url_for('index'))
    

默认情况下,session cookie会在用户关闭浏览器时删除。通过将 session.permanent属性设为True可以将session的有效期延长为 Flask.permanent_session_lifetime属性值对应的datetime.timedelta对象,也 可通过配置变量PERMANENT_SESSION_LIFETIME设置,默认为31 天。

g全局上下文

上下文件全局变量g可以在任何视图函数中使用, 为了方便 我们可以使用before_reequest钩子函数, 为g设置相应的值, 然后在各个视图函数中使用.

from flask import g
@app.before_request
def get_name():
  g.name = request.args.get('name')  #也可以像使用字典一个使用`g`
  
#在其它视图函数中可以直接引用g.name

返回上一页面

通过request对象中的referrer属性获取上一页面的URL(相对URL)

@app.route('/profile')
def profile():
  #if no Cookie, return to login page
  return redirect(request.referrer)
#request.referrer 返回的是绝对URL路径

模板

Jinja2模板: 最简单的模板就是一个包含响应文本的文件, 在模板中可以使用{{variable}}形式的变量进行占位. 默认flask程序在文件夹中的templates目录中查找模板.

三种基本定界符:

  • {% ... %} 语句, 如: if, for, set, ...
  • {{ ... }} 变量引用
  • {# ... #} 注释

Jinja2模板引擎: flask程序使用jinjia2模板引擎渲染模板, 使用相应的变量值替换模板中的占位符.

基本配置形式

模板1

...
<body>
    <h1>Hello World</h1>
</body>
...

模板2

...
<body>
    <h1>Hello, {{name}}</h1>
</body>
...

代码

from flask import Flask
from flask import render_template

app = Flask(__name__)

@app.route('/')
def index():
    #无变量模板
    return render_template('index.html')

@app.route('/login/<user>/')
def login(user):
    #可以使用Key=Value形式传递参数
    #return render_template('user.html', name=user)
    #也可以使用字典转换为关键字参数的形式传递变量
    return render_template('user.html', **{'name':user})

if __name__ == '__main__':
    app.run(host='0.0.0.0', debug=True)

变量

变量类型

Jinjia引擎支持多种类型的变量, 如: list dict object:

<p>A value from a dictionary: {{ mydict['key'] }}.</p> 
<p>A value from a list: {{ mylist[3] }}.</p> 
<p>A value from a list, with a variable index: \
    {{mylist[myintvar] }}.</p>
<p>A value from an object's method: {{ myobj.somemethod() }}.</p>

模板内自定义变量:

除了渲染时传入变量, 我们可以在模板内定义变量: {% set VarName = ... %}

{# 使用set标签定义变量 #}
{% set navigation = [('/', 'Home'), ('/about', 'About')] %}
{# 引用定义的变量 #}
{{navigation[0][1}}


{# 将一部分模板数据定义为变量 #}
{% set navigation %}
    <li><a href="/">Home</a>
    <li><a href="/about">About</a>
{% endset %}
{# 引用定义的变量 #}
{{navigation}}

内置上下文模版变量

变量 描述
config 当前配置对象
request 当前请求对象
session 当前会话对象
g 请求绑定的全局变量

自定义上下文

如果多个模板使用同一变量, 那么在不同视图函数中进行模板渲染时重复传入相同的变量比较麻烦, 我们可以使用app.context_processor装饰器来注册模板变量上下文处理函数, 在被装饰的函数中返回的变量,可以在所有模板中被使用.

@app.context_processor
def inject_foo():
  foo = "I am foo"
  return = dict(Var1=foo)  #等同于 return {'Var1': foo}

@app.route('/watchlist')
def watchlist():
  return render_template('watchlist.html')  
    #直接渲染, 无需传入变量, 因为该模板中用到的变量, 在上下文外理函数(inject_foo)中己经注册.

模板函数

内置全局函数

函数 描述
range([start,] stop [,step]) 和python中的range相同用法
lipsum(n=5, html=True, min=20, max=100) 生成随机文本, 默认生成5个paragraphs(段落), 每段20-100个词.
dict(**items) 和python中的dict用法相同
url_for() Flask在模版中内置的函数(非jinja2标准)
get_flashed_messages() Flask在模版中内置的函数(非jinja2标准), 用于获取flash消息

自定义全局模版函数

使用app.template_global装饰器将函数注册为模板全局函数.

@app.template_global()
def bar():
  return "I am bar."

#可以在装饰器传入可选参数name='FuncName', 为自定义函数设置在模版中调用的名字. 
@app.template_global(name='mybar')
def bar():
  return "<p>I am a bar.</p>"
#在模板中调用时应该使用mybar(), 而不是bar()
{{ mybar() }}

过滤器

也可以使用过滤器对变量进行修改:

过滤器名称 说明
safe 渲染值时不转义
capitalize 首字转大写,其它转小写
lower 把变量转小写
upper 把变量转大写
title 把变量中的每个单词首字母转大写
trim 把变量首尾空格去掉
striptags 渲染之前把变量中的所有HTML标签删掉

出于安全考虑, Jinja2 会 转义 所有变量。例 如, 如果一个变量的值为 \<h1>Hello</h1> , Jinja2 会将其渲染成 &lt;h1&gt;Hello&lt;/ h1&gt; ,浏览器能显示这个 h1 元素,但不会进行解释。很多情况下需要显示变量中存储 的 HTML 代码,这时就可使用 safe 过滤器。

代码示例

<body>
    <h1>Hello, {{name|capitalize}}</h1>
        <p>This is a value from a dictionary:{{d1['name']}} </p>
        <p>This is a value from a list: {{l1[1]|upper}}</p>
</body>

如果需要对一段内容进行过滤, 可以使用{% filter FILTER%} ... {% endfilter %}进行过滤.

{% filter upper %}
<p> Hello, I'm Liang </p>
{% endfilter %}

自定义过滤器

可以自定义函数,然后使用@app.template_filter()进行装饰.

from flask import Markup
@app.template_filter()
def musical(s):
  return s + Markup(' &#9835;')

控制结构

判断

    {% if name %}
        <h1>Hello, {{name|capitalize}}</h1>
    {% else %}
        <h1>Hello, Stranger</h1>
    {% endif %}

循环

   <ul>
        {% for i in list1 %}
            <li>{{i}}</li>
        {% endfor %}
    </ul>

局部模版

多个模版共有的内容可以定义为部局模版, 然后通过include引入 .

{# 为了和普通模板区分, 局部模版名称前加单下划线#}
{% include '_banner.html %}

将宏定义到一个单独文件. 该示例中文件名'macro.html'

{% macro render_comment(comments) %}
    <ul>
        {% for comment in comments %}
            <li>{{ comment }}</li>
        {% endfor %}
    </ul>
{% endmacro %}

在模板中import宏文件, 然后使用宏

<body>
{% import 'macro.html' as macros %}
<div>
    {{ macros.render_comment(comment_list)}}
</div>
</body>

测试程序:

from flask import Flask
from flask import render_template

l1 = ['alice','cindy', 'bob']
@app.route('/about/')
def about():
    return render_template('about.html', comment_list=l1)

模版继承

可以把网页的共同部分定义为基础模板, 其它子模板继承基模板. 例如: 页面上的导航栏, 页脚入在基模板中, 避免重复代码.

子模板继承基模板后, 会自动包含基模板内容和结构, 为了让子模板方便地覆盖或插入内容到基模板中,我们需要在基模 板中定义块(block),在子模板中可以通过定义同名的块来执行替换追加操作。

{% block body %}
...
{% endblock body %}

基模板示例:

<!DOCTYPE html>
<html lang="en">
<head>
    {% block head %}
        <meta charset="UTF-8">
        <title> {% block title %} Template_Title {% endblock %}</title>
        {% block styles %} {% endblock %}
    {% endblock %}
</head>
<body>
<nav>
    <ul><li><a href="{{ url_for('index') }}">Home</a> </li></ul>
</nav>

<main>
    {% block content %} {% endblock %}
</main>

<footer>
    {% block footer %}
        ...
    {% endblock %}
</footer>
{% block scripts %} {% endblock %}
</body>
</html>

子模板示例1:

{# 子模板第一行必须引入基模版 #}
{% extends 'base.html' %}  

{# 替换基模板中块的内容 #}
{% block content %}
    <h1> {{ user.username }}'s Watchlist</h1>
    <ul>
        {% for movie in movies %}
        <li> {{ movie.name }} --- {{ movie.year }}</li>
        {% endfor %}
    </ul>
{% endblock %}

子模板示例2:

{% extends 'base.html' %}

{% block content %}
    <h1>Home Page</h1>
    <ul>
        {{ lipsum() }}
    </ul>
{% endblock %}

也可以向基模板中追加内容:

{% block styles %}
{# 使用super(),表示基模板中该block的内容, 用于追加的操作 #}
{{ super() }}
<style>
    .foo {
        color: red;
    }
</style>
{% endblock %}

静态文件

静态文件指的是内容不需要动态生成的文件, 例如: 图片/CSS文件/JavaScript.

在 Flask 中,我们需要创建一个 static 文件夹来保存静态文件,它应该和程序模块、templates 文件夹在同一目录层级,所以我们在项目根目录创建它.

在 HTML 模板文件里,引入这些静态文件需要给出资源所在的 URL。为了更加灵活,这些文件的 URL 可以通过 Flask 提供的 url_for() 函数来生成.

图片

例如, 插入一个图片:

<img src="{{url_for('static', filename='avatar.jpg')}}" width="50">

上例中url_for()函数返回结果是'/static/foo.jpg'

CSS

例如, 插入一个CSS:

{% block styles %}
    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css')}}">
{% endblock %}

Favicon

网站的icon, 收藏夹头像. 一般为.ico格式. 存放在static目录下, 名为favicon.ico或favicon.png.

可以通过在模板页面的<head>部分添加一个link元素来实现.

<head>
  <link rel="icon" type="image/x-icon" href="{{ url_for("static", filename="favicon.ico")}}">
</head>

使用框架

手动编写CSS或是JS比较麻烦, 可以使用框架. 使用框架有两种方式:

  • 下载框架库, 并分类放置于static目录

    {% block styles %}
    <link rel="stylesheet" href="{{url_for('static',filename='css/bootstrap.min.css') }}"> 
    {% endblock %}
    
  • 直接引用框架CDN的URL

    {% block styles %} 
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
    {% endblock %}
    

使用宏加载框架, 可以避免重复编写引用框架库的代码.

{% macro static_file(type, filename_or_url, local=True) %}
    {% if local %}
        {% set filenaem_or_url = url_for("static", filename=filename_or_url) %}
    {% endif %}
    
    {% if type == 'css' %}
        <link rel="stylesheet" href="{{ filename_or_url }}" type="text/css">
    {% elif type == 'js' %}
        <script type="text/javascript" src="{{ filename_or_url }}"></script>
    {% elif type == 'icon' %}
        <link rel="icon" href="{{ filename_or_url }}">
    {% endif %}
{% endmacro %}

在模板中导入宏以后, 可以调用宏, 传入相关参数 ,直接得到引用框架的html代码

<head>
{% import 'macro.html' as macros %}
  {{ macros.static_file("css", "css/bootstrap.min.css") }}
</head>

自定义错误页

定义一个错误页模板

{% extends 'base.html' %}
{# 替换基模板中的块 #}
{% block title %} 404 - page not found {%  endblock %}

{% block content %}
    <h1>Page Not Found</h1>
    <p>You are lost...</p>
{%  endblock %}

使用@app.errorhandler() 装饰器注册自定义的错误处理函数

Web表单

简介

表单的标签为<form> ... </form>

示例:

<form method="get">
    <label for="username">Username</label><br>
    <input type="text" name="useranme" placeholder="Mark"><br>
    <label for="password">Password</label><br>
    <input type="password" name="password" placeholder="Zhang"><br>
    <input id="remember" name="remember" type="checkbox" checked>
    <label for="remember"><small>Remember</small></label><br>
    <input type="submit" name="submit" value="Log in">
</form>

Flask-WTF处理表单

WTForms支持在Python中使用类定义表单,然后直接通过类定义生 成对应的HTML代码.

扩展Flask-WTF集成了WTForms,使用它可以在Flask中更方便地使 用WTForms。Flask-WTF将表单数据解析、CSRF保护、文件上传等功能 与Flask集成,另外还附加了reCAPTCHA支持。

安装flask-wtf

使用pip安装flask-wtf包

pip3 install flask-wtf

Flask-WTF默认为每个表单启用CSRF保护, 默认情况下,Flask-WTF使用程序密钥来对CSRF令牌 进行签名,所以我们需要为程序设置密钥:

app.secret_key = 'secret string'

定义WTForms表单类

一个表单(form)由若干个输入字段(输入框,复选框,按键...)组成.

表单由Python的类表示, 这个类是继承了从WTForms导入的Form类.

表单中的输入字段由表单类的类属性表示.

示例

#导入表单基类Form, 以及表单中各输入组件类
from flask_wtf import Form, StringField, PasswordField, BooleanField, SubmitField
#导入表单输入组件的验证器
from wtforms.validators import DataRequired, Length

#定义一个自己的表单类, 继承了表单基类
class LoginForm(Form):
    #在自定义中添加输入组件, 并为输入组件设置HTML属性
    username = StringField('Username', validators=[DataRequired()])
    password = PasswordField('Password', validators=[DataRequired(), Length(8, 128)])
    remember = BooleanField('Remember me')
    submit = SubmitField('Login')

常用的表单组件

字段类 描述 对应的HTML表示
BooleanField checkbox, 值会被处理为True/False <input type="checkbox"/>
DateField
DateTimeField
文本字段,值处理为datetime.date
文本字段,值处理为datetime.datetime
<input type="text"/>
<input type="text"/><br />
FileField 文件上传字段 <input type="file"/>
FloatField 浮点数, 值被处理为浮点数 <input type="text"/><br />
IntegerField 整数, 值被处理为整数 <input type="text"/><br />
RadioField 单选按键 <input type="radio"/>
SelectField 下拉列表 <select><option>...</option></select>
SelectMultipleField 多选下拉列表 <select multiple><option>...</option></select>
SubmitField 提交按钮 <input type="summit"/>
StringField 文本字段 <input type="text"/>
HiddenField 隐藏文本字段 <input type="hidden">
PasswordField 密码文本 <input type="password"/>
TextAreaField 多选文本字段 <textarea> ... </textarea>

实例化表单中的字段类的常用参数

参数 描述
label <label>字段的值, 渲染后显示在输入字段前面的标签文字
render_kw 一个字典, 用设置表单字段的额外属性(字段类不支持所有HTML单表元素属性, 需要通过这个参数字典设置)
validators 一个列表, 包含一系列表单字段的验证器
default 字符串, 用来设置字段的默认值

输出HTML代码

#实例化自定义表单类
>>>myform = LoginForm()
#输入自定义表单类中的HTML元素代码
>>>myform.username()
'<input id="username" name="username" required type="text" value="">'
#输入自定义表单类中的HTML元素代码
>>>myform.submit()
'<input id="submit" name="submit" type="submit" value="Login">'
#生成表单中元素对应的标签代码(相应元素前边的标签名称)
>>>myform.username.label()
'<label for="username">Username</label>'

为表单元素设置额外属性

为表单元素设置例如: class, placeholder, style等属性

使用render_kw设置

在定义自定义类时, 为类中的元素属性传入render_kw参数 (字典型)

class LoginForm(Form):
    username = StringField('Username', validators=[DataRequired()], render_kw={'placeholder':'YourName'})
    password = PasswordField('Password', validators=[DataRequired(), Length(8, 128)], render_kw={'placeholder':'PASSWD'})

在调用元素方法时通过传入参数设置

在调用自定义类中的元素属性时, 为其传入参数(关键字参数)

#通过传入关键字参数, 为生成的HTML元素代码添加属性
>>>myform.username(class_="test", placeholder="YourName")
'<input class="test" id="username" name="username" placeholder="YourName" required type="text" value="">'

<div style="color:green"> class是python的关键字, 所以输入参数时使用class_代替class, 渲染后会自动更正</div>

在模板中渲染表单

#自定义表单类
class LoginForm(Form):
    username = StringField('Username', validators=[DataRequired()])
    password = PasswordField('Password', validators=[DataRequired(), Length(8, 128)])
    remember = BooleanField('Remember me')
    submit = SubmitField('Login')
#定义视图函数
@app.route('/login')
def login():
    #实例化表单类
    mf = LoginForm()
    #将实例化的表单类对象传给模板,进行渲染
    return render_template('login.html', form=mf)

模板文件

{% extends "base.html" %}

{% block content %}
    <form method="post">
    {# 在模板中调用元素方法时无需加括号,直接提供方法名即可 #}
    {# 如果需要添加额外HTML属性,也可以加括号以添加属性 #}
    {{ form.username.label }} {{ form.username }} <br/>
    {{ form.password.label }} {{ form.password }} <br/>
    {{ form.remember }} {{ form.remember.label }} <br/>
    {{ form.submit }}
    </form>
{% endblock %}

可以通过render_kw参数, 在模板文件中为HTML元素添加额外属性, 例如<class>, 通过bootstrap框架为表单元素设置样式

{% extends "base.html" %}
{# 通过替换基模板中的styles block,为本模板设置bootstrap框架 #}
{% block styles %}
    <link rel="stylesheet" href="{{ url_for('static', filename='bootstrap.min.css') }}"/>
{% endblock %}

{% block content %}
    <form method="post">
    {{ form.csrf_token }} <!---渲染CSRF令牌隐藏字段--->
    {# 为元素设置class属性, 以便应用style框架 #}
    {{ form.username.label }} {{ form.username(class='form-control') }} <br/>
    {{ form.password.label }} {{ form.password(class='form-control') }} <br/>
    {{ form.remember }} {{ form.remember.label }} <br/>
    {{ form.submit(class='btn btn-primary') }}
    </form>
{% endblock %}

处理表单数据

在表单中类型为"submit"的元素被点击时, 表单将被提交, 表单提交主要由三个属性控制:

属性 默认值 说明
action 当前URL,即页面对应的URL 表单提交时发送请求的URL
method GET 提交表单的方法, 常用GET和POST
enctype application/x-www-form-urlencoded 表单数据编码方式

GET方法仅支持3000个字符以内的提交, 并且暴露在URL中, 相比来说POST更安全.

表单验证

客户端验证

使用JS或HTML验证.

<!---HTML验证--->
<input type="text" name="username" required/>

服务端验证----WTForms验证机制

WTForms的验证方式是: 在实例化表单类时传入表单数据, 然后对类实例调用validate()方法, 这将会对提交换数据逐个调用元素字段实例化时定义的验证器, 验证结果以布尔值返回.

#使用wtforms, 提交表单时, 表单类实例不会自动接收提交的数据, 需要通过flask.request.form手工传入提交的数据
from wtforms import Form, StringField, PasswordField, BooleanField
>>> from wtforms.validators import DataRequired, Length

>>> class LoginForm(Form):
... username = StringField('Username', validators=[DataRequired()]) 
... password = PasswordField('Password', validators=[DataRequired() , Length(8, 128)])

#实例化表单类时,传入各字段待验证数据
>>> form = LoginForm(username='', password='123')
>>> form.data # 表单数据字典 {'username': '', 'password': '123'}
>>> form.validate() False
>>> form.errors # 错误消息字典 {'username': [u'This field is required.'], 'password': [u'Field must be at least 6 characters long.']}

<div style="color:red"><b>因为我们的表单使用POST方法提交,如果单纯使用WTForms,我 们在实例化表单类时需要首先把request.form传入表单类,而使用flask_wtf时,表单类继承的FlaskForm基类默认会从request.form获取表单数 据,所以不需要手动传入。</b></div>

#为了方便,一定要用flask_wtf
from flask_wtf import Form, StringField, PasswordField, BooleanField
>>> from wtforms.validators import DataRequired, Length

>>> class LoginForm(Form):
... username = StringField('Username', validators=[DataRequired()]) 
... password = PasswordField('Password', validators=[DataRequired() , Length(8, 128)])

@app.route('/login', methods=['GET', 'POST'])
def login():
    mf = LoginForm()
    if request.method == 'POST' and mf.valoidate():
        #deal with the posted data
        username = mf.username.data
        passwd = mf.password.data
        pass
        return redirect(url_for('index'))
    return render_template('login.html', form=mf)

上例中的if语句意思是:

if 用户提交表单 and 数据通过验证:
    获取并处理数据

Flask-WTF提供的validate_on_submit()方法合并了这两个操作, 因此上面的代码可以简化为:

@app.route('/login', methods=['GET', 'POST'])
def login():
    mf = LoginForm()
    if my.validate_on_submit():
        #deal with the posted data
        username = mf.username.data
        passwd = mf.password.data
        pass
        return redirect(url_for('index'))
    return render_template('login.html', form=mf)

通过表单类实例.表单元素.data获取相应表单元素的数据

在模板中处理错误消息

通过在模板中判断form.element.errors 是否为true,查看验证是否通过, 如果验证不通过, 渲染错误提示消息

{% block content %}
    <form method="post">
    {{ form.csrf_token }}
    {{ form.username.label }} {{ form.username(class='form-control') }} <br/>
        {% if form.username.errors %}
            {% for message in form.username.errors %}
                <small class="error">{{ message }}</small><br/>
            {% endfor %}
        {% endif %}
    {{ form.password.label }} {{ form.password(class='form-control') }} <br/>
        {% if form.password.errors %}
            {% for message in form.password.errors %}
                <small class="error">{{ message }}</small><br/>
            {% endfor %}
        {% endif %}
    {{ form.remember }} {{ form.remember.label }} <br/>
    {{ form.submit(class='btn btn-primary') }}
    </form>
{% endblock %}

自定义验证器

行内验证

在自定义的表单类中定义特殊名称的验证函数, 验证函数名称格式为:

validate_elementName(form, field): 名称为elementName的表单元素会自动被这个validate_elementName的函数验证, 该函数接收两个参数, 一个是表单类的实例,第二个是元素类的实例. 返回内容为验证异常类的实例.

有多少元素需要验证, 就需要定义多少验证函数.

class TestForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    password = PasswordField('Passowrd')
    remember = BooleanField('Remember me')
    submit = SubmitField('Login')

    def validate_password(form, field):
        if len(field.data) < 6:
            raise ValidationError("Passwd must be longer than 5")
全局验证器

只需要定义一个验证函数, 该验证函数只有两个要求:

  1. 接收两个参数: >表单类实例, >元素类实例
  2. 返回验证异常类的实例
#定义验证函数
from wtforms.validators import ValidationError
def my_validator(form, field):
  if len(field.data) < 6:
    raise ValidationError("passwd should longer than 5")
    
#使用验证函数
class my_form(FlaskForm):
  #验证器必须是可调用对象, 所以把验证器作为参数传入不可以带括号
  passwd = PasswordField('Passowrd', validators=[my_validator,])

上述的验证器可重用度不高, 实际环境中验证不同的元素可能返回的异常字符不一样, 并且验证内容和验证方式也不一样, 往往需要传入相应的参数,通知验证器如何验证.

这时我们需要把验证函数设计为工厂函数(一个可以产生验证器的函数:"产生函数的函数"):

# 定义验证器工厂函数
from wtforms.validators import ValidationError

def valid_generator(message=None):
  if message is None:
    message = "the input is invalidate"
  def _my_validator(form, field):
    if len(field.date) < 6:
      raise ValidationError(message)
  return _my_validator

# 使用验证函数
class my_form(FlaskForm):
  #验证器必须是可调用对象, 验证器工厂函数生成的就是可调用对象, 所以调用工厂函数时应该带括号.
  passwd = PasswordField('Passowrd', validators=[valid_generator('passwd should be at least 6!'),])

文件上传

定义上传表单:

我们使用Flask-WTF提供的FileField类, 而不是直接使用WTForms的FileField类. 但是它是继承于WTForms的FileField类的, 并添加了和Flask的集成.

from flask_wtf.file import FileField, FileRequired, FileAllowed
from flask_wtf import FlaskForm
from wtforms import SubmitField

# 定义一个上传文件的表单, 并验证文件名后缀
class UploadForm(FlaskForm):
    photo = FileField('Upload Image', validators=[FileRequired(), FileAllowed(['jpg', 'jpeg', 'png', 'gif'])])
    submit = SubmitField()
    
# 在模板中渲染表单
@app.route('/upload', methods=['GET', 'POST'])
def upload():
    mf = UploadForm()
    if mf.validate_on_submit():
        # deal with the uploaded file
        file = mf.upload.data
        pass
        return redirect(url_for('index'))
    return render_template('upload.html', form=mf)

上传文件的模板

注意表单元素的三个属性:

  1. action: 在哪个页面提交 (默认当前页)
  2. method: 默认GET, 我们需要用POST
  3. enctype: 上传数据编码方式, 必须是"multipart/form-data", 否则会把文件名作为表单数据提交.
{% extends "base.html" %}

{% block content %}
    <h1>Upload Page</h1>

{# 表单必须添加 method属性 和enctype属性, 否则无法上传 #}
<form method="post" enctype="multipart/form-data">
    {{ form.csrf_token }}
    {{ form.photo.label }} <br/>
    {{ form.photo }} <br/>
    {{ form.submit }}
</form>
{% endblock %}

处理上传文件

<div style="color:red"> 待续... </div>

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容