目录
- 本来以为Flask是用于后端,所以学会Python什么的就好。结果学起来才发现好多前端的东西和Flask 关系还是挺大的,例如HTML、例如HTTP。所以前面部分可能看得比较详细,后面实践部分就先略看过了,以后再来。
第01章 安装
- flask 是一个小型框架,甚至可称为“微框架”。但这并不意味它比其他框架功能少,它是一个可扩展的框架。flask 有一个包含基本服务的核心,其他功能可以通过扩展实现。
- 安装flask最便捷方式是用虚拟环境,也就是python解释器的一个私有副本。环境中可以安装私有包,且不会影响系统中安装的全局python解释器。
- 虚拟环境安装:
①通过 virtualenv --version 查看是否安装过virtualenv,出错则没有安装
②apt-get install python-virtualenv 安装 - flask安装:pip install flask
- 如果要安装python3可用的flask,通过pip3 install flask
- 想知道是否正确安装flask,进入python,然后import flask看有没有错误提醒
第02章 程序的基本结构
- flask程序,需要创建一个Flask类对象。
- Flask 类的构造函数只有一个必须指定的参数,即程序主模块或包的名字。在大多数程序
中,Python 的__name__ 变量就是所需的值。
from flask import Flask
app = Flask(__name__)
- 客户端(例如Web 浏览器)把请求发送给Web 服务器,Web 服务器再把请求发送给Flask程序实例。
- 程序实例需要知道对每个URL 请求运行哪些代码,所以保存了一个URL 到Python 函数的映射关系。处理URL 和函数之间关系的程序称为路由。
- 下面例子,把index函数注册为程序根地址的处理程序。例如部署该程序的服务器域名为www.example.com,在浏览器中访问http://www.example.com 后,会触发服务器执行index() 函数。
像index()这样的函数称为视图函数(view function),函数的返回值称为响应,是客户端会收到的内容。
@app.route('/')
def index():
return '<h1>Hello World!</h1>'
- 下列尖括号里的内容是动态部分
@app.route('/user/<name>')
def user(name):
return '<h1>Hello, %s!</h1>' % name
- 路由中的动态部分默认使用字符串,不过也可使用类型定义。例如,路由/user/<int:id>只会匹配动态片段id 为整数的URL。Flask 支持在路由中使用int、float 和path 类型。path 类型也是字符串,但不把斜线视作分隔符,而将其当作动态片段的一部分。
- 通过run方法启动flask集成的开发Web服务器。__name__=='__main__' 是Python 的惯常用法,在这里确保直接执行这个脚本时才启动开发Web 服务器。如果这个脚本由其他脚本引入,程序假定父级脚本会启动不同的服务器,因此不会执行app.run()。
if __name__ == '__main__':
app.run(debug=True)
- 一个完整的flask程序(hello.py,下文会有import hello)
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return '<h1>Hello World!</h1>'
@app.route('/user/<name>')
def user(name):
return '<h1>Hello,%s!</h1>' % name
if __name__ == '__main__':
app.run(debug=True)
- 从客户端收到请求时,需要让视图函数能访问请求对象(封装了客户端发送的HTTP请求)。可以把请求对象作为参数传入视图函数,但这样会让视图函数变得很乱。为此,Flask使用上下文临时把某些对象变为全局可访问。
- Flask中有两种上下文:程序上下文和请求上下文
变量名 | 上下文 | 说明 |
---|---|---|
current_app | 程序上下文 | 当前激活程序的程序实例 |
g | 程序上下文 | 处理请求时用作临时存储对象,每次请求都会重设这个变量 |
request | 请求上下文 | 请求对象,封装了客户端发出的HTTP请求中的内容 |
session | 请求上下文 | 用户会话,用于存储请求之间需要“记住”的值的词典 |
- 一个使用request的例子
from flask import request
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
user_agent = request.headers.get('User-Agent')
return '<p>Your browser is %s</p>' % user_agent
if __name__ == '__main__':
app.run(debug=True)
- 程序上下文使用方法(python shell)
>>> from hello import app
>>> from flask import current_app
>>> current_app.name #此时使用会出错
>>> app_ctx = app.app_context()
>>> app_ctx.push()
>>> current_app.name #'hello'
>>> app_ctx.pop()
- 请求调度:程序收到客户端发来的请求时,要找到处理该请求的视图函数。为了完成这个任务,Flask会在程序的URL 映射中查找请求的URL。URL 映射是URL 和视图函数之间的对应关系。Flask 使用app.route 修饰器或者非修饰器形式的app.add_url_rule() 生成映射。
- 下图所示:
①/ 和/user/<name> 路由在程序中使用app.route 修饰器定义。/static/<filename> 路由是Flask 添加的特殊路由,用于访问静态文件(第3章)。
②URL 映射中的HEAD、Options、GET 是请求方法,由路由进行处理。Flask 为每个路由都指定了请求方法,这样不同的请求方法发送到相同的URL 上时,会使用不同的视图函数进行处理。HEAD 和OPTIONS 方法由Flask 自动处理,因此可以这么说,在这个程序中,URL映射中的3 个路由都使用GET 方法。
- 为了避免在每个视图函数中都使用重复的代码,Flask 提供了注册通用函数的功能,注册的函数可在请求被分发到视图函数之前或之后调用。
- 请求钩子使用修饰器实现。Flask 支持以下4 种钩子:
before_first_request:注册一个函数,在处理第一个请求之前运行。
before_request:注册一个函数,在每次请求之前运行。
after_request:注册一个函数,如果没有未处理的异常抛出,在每次请求之后运行。
teardown_request:注册一个函数,即使有未处理的异常抛出,也在每次请求之后运行。 - 在请求钩子函数和视图函数之间共享数据一般使用上下文全局变量g。
- 视图函数的返回值会作为响应内容。除了字符串返回值,HTTP 响应中一个很重要的部分是状态码,Flask 默认设为200,这个代码表明请求已经被成功处理。如果视图函数返回的响应需要使用不同的状态码,那么可以把数字代码作为第二个返回值,添加到响应文本之后。例如,下述视图函数返回一个400 状态码,表示请求无效:
@app.route('/')
def index():
return '<h1>Bad Request</h1>', 400
- 如果不想返回由1 个、2 个或3 个值组成的元组,Flask 视图函数还可以返回Response 对象。make_response() 函数可接受1 个、2 个或3 个参数(和视图函数的返回值一样),并返回一个Response 对象。
- 下例创建了一个响应对象,然后设置了cookie:
from flask import make_response
@app.route('/')
def index():
response = make_response('<h1>This document carries a cookie!</h1>')
response.set_cookie('answer', '42')
return response
- 有一种名为重定向的特殊响应类型。这种响应没有页面文档,只告诉浏览器一个新地址用以加载新页面。重定向经常在Web 表单中使用(第4章)。
- 重定向经常使用302 状态码表示,指向的地址由Location 首部提供。重定向响应可以使用3 个值形式的返回值生成,也可在Response 对象中设定。不过,由于使用频繁,Flask 提供了redirect() 辅助函数,用于生成这种响应:
from flask import redirect
@app.route('/')
def index():
return redirect('http://www.example.com')
- 还有一种特殊的响应由abort 函数生成,用于处理错误。在下面这个例子中,如果URL 中动态参数id 对应的用户不存在,就返回状态码404:
from flask import abort
@app.route('/user/<id>')
def get_user(id):
user = load_user(id)
if not user:
abort(404)
return '<h1>Hello, %s</h1>' % user.name
- Flask 的开发Web 服务器支持很多启动设置选项,但只能在脚本中作为参数传给app.run()函数。这种方式并不十分方便,传递设置选项的理想方式是使用命令行参数。
- Flask-Script 是一个Flask 扩展,为Flask 程序添加了一个命令行解析器。Flask-Script 自带了一组常用选项,而且还支持自定义命令。
- 安装:pip install flask-script
- 使用例子:
from flask import Flask
from flask_script import Manager #此处和书中不同,是因为版本修改了,名字也不同了
app = Flask(__name__)
manager = Manager(app)
@app.route('/')
def index():
return '<h1>Hello World!</h1>'
@app.route('/user/<name>')
def user(name):
return '<h1>Hello,%s!</h1>' % name
if __name__ == '__main__':
manager.run()
- 这种情况下,运行会提示你部分可用的命令行
- shell 命令用于在程序的上下文中启动Python shell 会话。你可以使用这个会话中运行维护任务或测试,还可调试异常。
- runserver 命令用来启动Web 服务器。运行python hello.py runserver 将以调试模式启动Web 服务器
- --help可用看到更多命令行提示:
第03章 模版
- 用户在网站中注册了一个新账户。相应的视图函数需要访问数据库,添加新用户,然后生成响应回送浏览器。这两个过程分别称为业务逻辑和表现逻辑。(业务逻辑可以理解为处理的过程,表现逻辑是返回响应的过程)
- 把业务逻辑和表现逻辑混在一起会导致代码难以理解和维护。把表现逻辑移到模板中能够提升程序的可维护性。
- 模板是一个包含响应文本的文件,其中包含用占位变量表示的动态部分,其具体值只在请求的上下文中才能知道。使用真实值替换变量,再返回最终得到的响应字符串,这一过程称为渲染。
- 为了渲染模板,Flask 使用了一个名为Jinja2 的强大模板引擎。
- 默认情况下,Flask 在程序文件夹中的templates 子文件夹中寻找模板。
- Flask 提供的render_template 函数把Jinja2 模板引擎集成到了程序中。render_template 函数的第一个参数是模板的文件名。随后的参数都是键值对,表示模板中变量对应的真实值。在这段代码中,第二个模板收到一个名为name 的变量。
name=name 是关键字参数,这类关键字参数很常见,但如果你不熟悉它们的话,可能会觉得迷惑且难以理解。左边的“name”表示参数名,就是模板中使用的占位符;右边的“name”是当前作用域中的变量,表示同名参数的值。
from flask import Flask, render_template
@app.route('/')
def index():
return render_template('index.html')
@app.route('/user/<name>')
def user(name):
return render_template('user.html', name=name)
- 在模板中使用的{{ name }} 结构表示一个变量,它是一种特殊的占位符,告诉模板引擎这个位置的值从渲染模板时使用的数据中获取。
<h1>Hello, {{ name }}!</h1>
- Jinja2 能识别所有类型的变量,甚至是一些复杂的类型,例如列表、字典和对象。
<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>
- 可以使用过滤器修改变量,过滤器名添加在变量名之后,中间使用竖线分隔。例如,下述模板以首字母大写形式显示变量name 的值:
Hello, {{ name|capitalize }}
- Jinja2 提供的部分常用过滤器:
过滤器名 | 说明 |
---|---|
safe | 渲染值时不转义 |
capitalize | 把值的首字母转换为大写,其他字母转换为小写 |
lower | 把值转为小写形式 |
upper | 把值转换成大写形式 |
title | 把值中每个单词的首字母都转换成大写 |
trim | 把值的首尾空格去掉 |
striptags | 渲染之前把值中所以的HTML标签删掉 |
- Jinja2 提供了多种控制结构,可用来改变模板的渲染流程。
- 条件控制语句
{% if user %}
Hello, {{ user }}!
{% else %}
Hello, Stranger!
{% endif %}
- for循环语句
<ul>
{% for comment in comments %}
<li>{{ comment }}</li>
{% endfor %}
</ul>
- 可支持宏
{% macro render_comment(comment) %}
<li>{{ comment }}</li>
{% endmacro %}
<ul>
{% for comment in comments %}
{{ render_comment(comment) }}
{% endfor %}
</ul>
- 可以将宏保存在单独的文件中,然后在需要的模版中导入,以便重复使用:
{% import 'macros.html' as macros %}
<ul>
{% for comment in comments %}
{{ macros.render_comment(comment) }}
{% endfor %}
</ul>
- 模版的继承:
①名为base.html的模版:block 标签定义的元素可在衍生模板中修改。在本例中,我们定义了名为head、title 和body 的块。注意,title 包含在head 中。
②上面基模版的衍生模版:extends 指令声明这个模板衍生自base.html。在extends 指令之后,基模板中的3 个块被重新定义,模板引擎会将其插入适当的位置。注意新定义的head 块,在基模板中其内容不是空的,所以使用super() 获取原来的内容。<html> <head> {% block head %} <title>{% block title %}{% endblock %} - My Application</title> {% endblock %} </head> <body> {% block body %} {% endblock %} </body> </html>
{% extends "base.html" %} {% block title %}Index{% endblock %} {% block head %} {{ super() }} <style> </style> {% endblock %} {% block body %} <h1>Hello, World!</h1> {% endblock %}
- 使用Flask-Bootstrap集成Twitter Bootstrap。Bootstrap 是Twitter 开发的一个开源框架。
- 安装:pip install flask-bootstrap
- 初始化:
from flask_bootstrap import Bootstrap
# ...
bootstrap = Bootstrap(app)
- 初始化Flask-Bootstrap 之后,就可以在程序中使用一个包含所有Bootstrap 文件的基模板。这个模板利用Jinja2 的模板继承机制,让程序扩展一个具有基本页面结构的基模板,其中就有用来引入Bootstrap 的元素。把user.html 改写为衍生模板后的新版本:
{% extends "bootstrap/base.html" %}
{% block title %}Flasky{% endblock %}
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle"
data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Flasky</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">Home</a></li>
</ul>
</div>
</div>
</div>
{% endblock %}
{% block content %}
<div class="container">
<div class="page-header">
<h1>Hello, {{ name }}!</h1>
</div>
</div>
{% endblock %}
- 像常规路由一样,Flask 允许程序使用基于模板的自定义错误页面。最常见的错误代码有两个:404,客户端请求未知页面或路由时显示;500,有未处理的异常时显示。为这两个错误代码指定自定义处理程序的方式如下所示。
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
- 在模板中直接编写简单路由的URL 链接不难,但对于包含可变部分的动态路由,在模板中构建正确的URL 就很困难。而且,直接编写URL 会对代码中定义的路由产生不必要的赖关系。如果重新定义路由,模板中的链接可能会失效。
- 为了避免这些问题,Flask 提供了url_for() 辅助函数,它可以使用程序URL 映射中保存的信息生成URL。
- url_for() 函数最简单的用法是以视图函数名(或者app.add_url_route() 定义路由时使用的端点名)作为参数,返回对应的URL。
- 使用url_for() 生成动态地址时, 将动态部分作为关键字参数传入。例如,url_for('user', name='john', _external=True) 的返回结果是http://localhost:5000/user/john。
- 传入url_for() 的关键字参数不仅限于动态路由中的参数。函数能将任何额外参数添加到查询字符串中。例如,url_for('index', page=2) 的返回结果是/?page=2。
- Web 程序不是仅由Python 代码和模板组成。大多数程序还会使用静态文件,例如HTML代码中引用的图片、JavaScript 源码文件和CSS。
- 服务器需要统一时间单位,这和用户所在的地理位置无关,所以一般使用协调世界时(Coordinated Universal Time,UTC)。不过用户看到UTC 格式的时间会感到困惑,他们更希望看到当地时间,而且采用当地惯用的格式。
- 要想在服务器上只使用UTC 时间,一个优雅的解决方案是,把时间单位发送给Web 浏览器,转换成当地时间,然后渲染。Web 浏览器可以更好地完成这一任务,因为它能获取用户电脑中的时区和区域设置。
第04章 Web表单
- request.form 能获取POST 请求中提交的表单数据。
- 尽管Flask 的请求对象提供的信息足够用于处理Web 表单,但有些任务很单调,而且要重复操作。比如,生成表单的HTML 代码和验证提交的表单数据。Flask-WTF 扩展可以把处理Web 表单的过程变成一种愉悦的体验。
- Flask-WTF 及其依赖可使用pip 安装:pip install flask-wtf
- 默认情况下,Flask-WTF 能保护所有表单免受跨站请求伪造(Cross-Site Request Forgery,CSRF)的攻击。为了实现CSRF 保护,Flask-WTF 需要程序设置一个密钥。Flask-WTF 使用这个密钥生成加密令牌,再用令牌验证请求中表单数据的真伪。
- 设置密钥的方法:
app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'
- 为了增强安全性,密钥不应该直接写入代码,而要保存在环境变量中。这一技术会在第7 章介绍。
- 使用Flask-WTF 时,每个Web 表单都由一个继承自Form 的类表示。这个类定义表单中的一组字段,每个字段都用对象表示。字段对象可附属一个或多个验证函数。验证函数用来验证用户提交的输入值是否符合要求。
- 这个表单中的字段都定义为类变量,类变量的值是相应字段类型的对象。在这个示例中,NameForm 表单中有一个名为name 的文本字段和一个名为submit 的提交按钮。StringField类表示属性为type="text" 的<input> 元素。SubmitField 类表示属性为type="submit" 的<input> 元素。字段构造函数的第一个参数是把表单渲染成HTML 时使用的标号。
StringField 构造函数中的可选参数validators 指定一个由验证函数组成的列表,在接受用户提交的数据之前验证数据。验证函数Required() 确保提交的字段不为空。
from flask_wtf import Form
from wtforms import StringField, SubmitField
from wtforms.validators import Required
class NameForm(Form):
name = StringField('What is your name?', validators=[Required()])
submit = SubmitField('Submit')
- WTForm支持的HTML标准字段:
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:一组指定类型的字段
- WTForms 内建的验证函数:
Email:验证电子邮件地址
EqualTo:比较两个字段的值;常用于要求输入两次密码进行确认的情况
IPAddress:验证IPv4 网络地址
Length:验证输入字符串的长度
NumberRange:验证输入的值在数字范围内
Optional:无输入值时跳过其他验证函数
Required:确保字段中有数据
Regexp:使用正则表达式验证输入值
URL:验证URL
AnyOf:确保输入值在可选值列表中
NoneOf:确保输入值不在可选值列表中
- 表单字段是可调用的,在模板中调用后会渲染成HTML。假设视图函数把一个NameForm 实例通过参数form 传入模板,在模板中可以生成一个简单的表单,如下所示:
<form method="POST">
{{ form.hidden_tag() }}
{{ form.name.label }} {{ form.name() }}
{{ form.submit() }}
</form>
- 以下解释的完整html和py文件看后面
- app.route 修饰器中添加的methods 参数告诉Flask 在URL 映射中把这个视图函数注册为GET 和POST 请求的处理程序。如果没指定methods 参数,就只把视图函数注册为GET 请求的处理程序。
- 把POST 加入方法列表很有必要,因为将提交表单作为POST 请求进行处理更加便利。表单也可作为GET 请求提交,不过GET 请求没有主体,提交的数据以查询字符串的形式附加到URL 中,可在浏览器的地址栏中看到。基于这个以及其他多个原因,提交表单大都作为POST 请求进行处理。
- 局部变量name 用来存放表单中输入的有效名字,如果没有输入,其值为None。如上述代码所示,在视图函数中创建一个NameForm 类实例用于表示表单。提交表单后,如果数据能被所有验证函数接受,那么validate_on_submit() 方法的返回值为True,否则返回False。这个函数的返回值决定是重新渲染表单还是处理表单提交的数据。
用户第一次访问程序时,服务器会收到一个没有表单数据的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 的值为表单中输入的名字,因此会显示一个针对该用户的欢迎消息。 - flask_test.py
from flask import Flask, request, render_template
from flask_wtf import Form
from wtforms import StringField, SubmitField
from wtforms.validators import Required
from flask_bootstrap import Bootstrap
app = Flask(__name__)
bootstrap = Bootstrap(app)
class NameForm(Form):
name = StringField('What is your name?', validators=[Required()])
submit = SubmitField('Submit')
@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)
if __name__ == '__main__':
app.run(debug=True)
- index.html
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Hello, {% if name %}{{ name }}{% else %}Stranger{% endif %}!</h1>
</div>
{{ wtf.quick_form(form) }}
{% endblock %}
- base.html
{% extends "bootstrap/base.html" %}
{% block title %}Flasky{% endblock %}
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle"
data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Flasky</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">Home</a></li>
</ul>
</div>
</div>
</div>
{% endblock %}
{% block content %}
<div class="container">
{% block page_content %}{% endblock %}
</div>
{% endblock %}
- 最终显示
- 刷新页面时浏览器会重新发送之前已经发送过的最后一个请求。如果这个请求是一个包含表单数据的POST 请求,刷新页面后会再次提交表单。最好别让Web 程序把POST 请求作为浏览器发送的最后一个请求。这种需求的实现方式是,使用重定向作为POST 请求的响应,而不是使用常规响应。
- 重定向原理:使用重定向作为POST 请求的响应,而不是使用常规响应。重定向是一种特殊的响应,响应内容是URL,而不是包含HTML 代码的字符串。浏览器收到这种响应时,会向重定向的URL 发起GET 请求,显示页面的内容。这个页面的加载可能要多花几微秒,因为要先把第二个请求发给服务器。除此之外,用户不会察觉到有什么不同。这种情况保证:最后一个请求是GET 请求,所以刷新命令能像预期的那样正常使用了。这个技巧称为Post/ 重定向/Get 模式。
- 但这种方法会带来另一个问题。程序处理POST 请求时,使用form.name.data 获取用户输入的名字,可是一旦这个请求结束,数据也就丢失了。因为这个POST 请求使用重定向处理,所以程序需要保存输入的名字。
程序可以把数据存储在用户会话中,在请求之间“记住”数据。用户会话是一种私有存储,存在于每个连接到服务器的客户端中。我们在第2 章介绍过用户会话,它是请求上下文中的变量,名为session,像标准的Python 字典一样操作。 - 包含合法表单数据的请求最后会调用redirect() 函数。redirect() 是个辅助函数,用来生成HTTP 重定向响应。redirect() 函数的参数是重定向的URL,这里使用的重定向URL 是程序的根地址,因此重定向响应本可以写得更简单一些,写成redirect('/'),但却会使用Flask 提供的URL 生成函数url_for()。推荐使用url_for() 生成URL,因为这个函数使用URL 映射生成URL,从而保证URL 和定义的路由兼容,而且修改路由名字后依然可用。
-
from flask import Flask, render_template, session, redirect, url_for @app.route('/', methods=['GET', 'POST']) def index(): 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'))
第05章 数据库
- 数据库按照一定规则保存程序数据,程序再发起查询取回所需的数据。Web 程序最常用基于关系模型的数据库,这种数据库也称为SQL 数据库,因为它们使用结构化查询语言。不过最近几年文档数据库和键值对数据库成了流行的替代选择,这两种数据库合称NoSQL数据库。
- NoSQL 数据库一般使用集合代替表,使用文档代替记录。
- Flask-SQLAlchemy 是一个Flask 扩展,简化了在Flask 程序中使用SQLAlchemy 的操作。SQLAlchemy 是一个很强大的关系型数据库框架,支持多种数据库后台。SQLAlchemy 提供了高层ORM,也提供了使用数据库原生SQL 的低层功能。
- 和其他大多数扩展一样,Flask-SQLAlchemy 也使用pip 安装:pip install flask-sqlalchemy
- 配置数据库:
from flask_sqlalchemy import SQLAlchemy
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] =\
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
db = SQLAlchemy(app)
- 定义模型:
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
def __repr__(self):
return '<Role %r>' % self.name
users = db.relationship('User', backref='role')
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)
def __repr__(self):
return '<User %r>' % self.username
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
- 创建表:
db.create_all() - 插入行:
①定义数据
admin_role = Role(name='Admin')
mod_role = Role(name='Moderator')
user_role = Role(name='User')
user_john = User(username='john', role=admin_role)
user_susan = User(username='susan', role=user_role)
user_david = User(username='david', role=user_role)
②插入
db.session.add(admin_role)
db.session.add(mod_role)
db.session.add(user_role)
db.session.add(user_john)
db.session.add(user_susan)
db.session.add(user_david)
或者
db.session.add_all([admin_role, mod_role, user_role,user_john, user_susan, user_david])
③提交
db.session.commit() - 视图函数中操作数据库
@app.route('/', methods=['GET', 'POST'])
def index():
form = NameForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.name.data).first()
if user is None:
user = User(username = form.name.data)
db.session.add(user)
session['known'] = False
else:
session['known'] = True
session['name'] = form.name.data
form.name.data = ''
return redirect(url_for('index'))
return render_template('index.html',
form = form, name = session.get('name'),
known = session.get('known', False))
第06章 电子邮件
- 虽然Python 标准库中的smtplib 包可用在Flask 程序中发送电子邮件,但包装了smtplib 的Flask-Mail 扩展能更好地和Flask 集成。
- 安装Flask-Mail:pip install flask-mail
- Flask-Mail SMTP服务器的配置:
配 | 置默认值 | 说明 |
---|---|---|
MAIL_SERVER | localhost | 电子邮件服务器的主机名或IP 地址 |
MAIL_PORT | 25 | 电子邮件服务器的端口 |
MAIL_USE_TLS | False | 启用传输层安全(Transport Layer Security)协议 |
MAIL_USE_SSL | False | 启用安全套接层(Secure Sockets Layer)协议 |
MAIL_USERNAME | None | 邮件账户的用户名 |
MAIL_PASSWORD | None | 邮件账户的密码 |
- 使用谷歌邮件发送邮件:
from flask_mail import Mail
import os
# ...
mail = Mail(app)
# ...
#设置邮件发送者
app.config['MAIL_SERVER'] = 'smtp.googlemail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
#发送一封邮件
msg = Message('test subject', sender='you@example.com',recipients=['you@example.com'])
msg.body = 'text body'
msg.html = '<b>HTML</b> body'
with app.app_context():
mail.send(msg)
- 千万不要把账户密令直接写入脚本,特别是当你计划开源自己的作品时。为了保护账户信息,你需要让脚本从环境中导入敏感信息。
- Flask-Mail 中的send() 函数使用current_app,因此要在激活的程序上下文中执行。
- 在程序函数中集成发电子邮件功能:
from flask_mail import Message
app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin <flasky@example.com>'
def send_email(to, subject, template, **kwargs):
msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject,sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])
msg.body = render_template(template + '.txt', **kwargs)
msg.html = render_template(template + '.html', **kwargs)
mail.send(msg)
第07章 大型程序的结构
- 多文件Flask程序组织结构:
|-flasky
|-app/
|-templates/
|-static/
|-main/
|-__init__.py
|-errors.py
|-forms.py
|-views.py
|-__init__.py
|-email.py
|-models.py
|-migrations/
|-tests/
|-__init__.py
|-test*.py
|-venv/
|-requirements.txt
|-config.py
|-manage.py
- Flask程序一般都保存在名为app 的包中;migrations文件夹包含数据库迁移脚本;单元测试编写在 tests包中;venv文件夹包含 Python 虚拟环境。
- requirements.txt列出了所有依赖包,便于在其他电脑中重新生成相同的虚拟环境;config.py 存储配置;manage.py用于启动程序以及其他的程序任务。
第08章 用户认证
- 大多数程序都要进行用户跟踪。用户连接程序时会进行身份认证,通过这一过程,让程序知道自己的身份。程序知道用户是谁后,就能提供有针对性的体验。
- 优秀的Python 认证包很多,但没有一个能实现所有功能。本章介绍的认证方案使用了多个包,并编写了胶水代码让其良好协作。本章使用的包列表如下。
Flask-Login:管理已登录用户的用户会话。
Werkzeug:计算密码散列值并进行核对。
itsdangerous:生成并核对加密安全令牌。
- 若想保证数据库中用户密码的安全,关键在于不能存储密码本身,而要存储密码的散列值。计算密码散列值的函数接收密码作为输入,使用一种或多种加密算法转换密码,最终得到一个和原始密码没有关系的字符序列。核对密码时,密码散列值可代替原始密码,因为计算散列值的函数是可复现的:只要输入一样,结果就一样。
- Werkzeug 中的security 模块能够很方便地实现密码散列值的计算。这一功能的实现只需要两个函数,分别用在注册用户和验证用户阶段。
- generate_password_hash(password, method=pbkdf2:sha1, salt_length=8):这个函数将原始密码作为输入,以字符串形式输出密码的散列值,输出的值可保存在用户数据库中。method 和salt_length 的默认值就能满足大多数需求。
- check_password_hash(hash, password):这个函数的参数是从数据库中取回的密码散列值和用户输入的密码。返回值为True 表明密码正确。
- 用户登录程序后,他们的认证状态要被记录下来,这样浏览不同的页面时才能记住这个状态。Flask-Login 是个非常有用的小型扩展,专门用来管理用户认证系统中的认证状态,且不依赖特定的认证机制。
- 使用之前,我们要安装这个扩展:pip install flask-login
- 要想使用Flask-Login 扩展,程序的User 模型必须实现几个方法:
方法 | 说明 |
---|---|
is_authenticated() | 如果用户已经登录,必须返回True,否则返回False |
is_active() | 如果允许用户登录,必须返回True,否则返回False。如果要禁用账户,可以返回False |
is_anonymous() | 对普通用户必须返回False |
get_id() | 必须返回用户的唯一标识符,使用Unicode 编码字符串 |
- 这4 个方法可以在模型类中作为方法直接实现,不过还有一种更简单的替代方案。Flask-Login 提供了一个UserMixin 类,其中包含这些方法的默认实现,且能满足大多数需求。
from flask.ext.login import UserMixin
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key = True)
email = db.Column(db.String(64), unique=True, index=True)
username = db.Column(db.String(64), unique=True, index=True)
password_hash = db.Column(db.String(128))
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
- Flask-Login 在程序的工厂函数中初始化,LoginManager 对象的session_protection 属性可以设为None、'basic' 或'strong',以提
供不同的安全等级防止用户会话遭篡改。设为'strong' 时,Flask-Login 会记录客户端IP地址和浏览器的用户代理信息,如果发现异动就登出用户。login_view 属性设置登录页面的端点。回忆一下,登录路由在蓝本中定义,因此要在前面加上蓝本的名字。
from flask.ext.login import LoginManager
login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'auth.login'
def create_app(config_name):
# ...
login_manager.init_app(app)
# ...
- 如果新用户想成为程序的成员,必须在程序中注册,这样程序才能识别并登入用户。程序的登录页面中要显示一个链接,把用户带到注册页面,让用户输入电子邮件地址、用户名和密码。
- 注册页面使用的表单要求用户输入电子邮件地址、用户名和密码。
from flask_wtf import Form
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import Required, Length, Email, Regexp, EqualTo
from wtforms import ValidationError
from ..models import User
class RegistrationForm(Form):
email = StringField('Email', validators=[Required(), Length(1, 64),
Email()])
username = StringField('Username', validators=[
Required(), Length(1, 64), Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,
'Usernames must have only letters, '
'numbers, dots or underscores')])
password = PasswordField('Password', validators=[
Required(), EqualTo('password2', message='Passwords must match.')])
password2 = PasswordField('Confirm password', validators=[Required()])
submit = SubmitField('Register')
def validate_email(self, field):
if User.query.filter_by(email=field.data).first():
raise ValidationError('Email already registered.')
def validate_username(self, field):
if User.query.filter_by(username=field.data).first():
raise ValidationError('Username already in use.')
- 这个表单使用WTForms 提供的Regexp 验证函数,确保username 字段只包含字母、数字、下划线和点号。
- 安全起见,密码要输入两次。此时要验证两个密码字段中的值是否一致,这种验证可使用WTForms 提供的另一验证函数实现,即EqualTo。
第09章 用户角色
- Web 程序中的用户并非都具有同样地位。有多种方法可用于在程序中实现角色。具体采用何种实现方法取决于所需角色的数量和细
分程度。 - 简单的程序可能只需要两个角色,一个表示普通用户,一个表示管理员。对于这种情况,在User 模型中添加一个is_administrator 布尔值字段就足够了。
- 复杂的程序可能需要在普通用户和管理员之间再细分出多个不同等级的角色。有些程序甚至不能使用分立的角色,这时赋予用户某些权限的组合或许更合适。
第15章 测试
- 编写测试组件很重要,但知道测试的好坏同样重要。代码覆盖工具用来统计单元测试检查了多少程序功能,并提供一个详细的报告,说明程序的哪些代码没有测试到。这个信息非常重要,因为它能指引你为最需要测试的部分编写新测试。
- Python 提供了一个优秀的代码覆盖工具,称为coverage,你可以使用pip 进行安装:
pip install coverage - 这个工具本身是一个命令行脚本,可在任何一个Python 程序中检查代码覆盖。除此之外,它还提供了更方便的脚本访问功能,使用编程方式启动覆盖检查引擎。
- 程序的某些代码严重依赖运行中的程序所创建的环境。例如,你不能直接调用视图函数中的代码进行测试,因为这个函数可能需要访问Flask 上下文全局变量,如request 或session;视图函数可能还等待接收POST 请求中的表单数据,而且某些视图函数要求用户先登录。简而言之,视图函数只能在请求上下文和运行中的程序里运行。
- Flask 内建了一个测试客户端用于解决(至少部分解决)这一问题。测试客户端能复现程序运行在Web 服务器中的环境,让测试扮演成客户端从而发送请求。
第16章 性能
- 如果程序性能随着时间推移不断降低,那很有可能是因为数据库查询变慢了,随着数据库规模的增长,这一情况还会变得更糟。优化数据库有时很简单,只需添加更多的索引即可;有时却很复杂,需要在程序和数据库之间加入缓存。大多数数据库查询语言都提供了explain 语句,用来显示数据库执行查询时采取的步骤。从这些步骤中,我们经常能发现数据库或索引设计的不足之处。