请求钩子
flask没有django中的中间件,但是却有类似的机制(在请求之前做点事,请求完成之后再做点事)。flask给我们预留的钩子能完成这些事。对于钩子的简单理解:flask预留了一些占位的空白空间,当我们往这段空间放代码的时候,那么流程在走的时候就会经过我们的代码。钩子的形象意义就是一段代码执行的时候,会顺带着执行钩子上的一系列代码,而不是单纯的那一段代码。
before_first_request:在处理第一个请求前运行。
before_request:在每次请求前运行。
after_request(response):如果没有未处理的异常抛出,在每次请求后运行。
teardown_request(response):在每次请求后运行,即使有未处理的异常抛出,在这个钩子里并不能捕获异常进行处理,而且一旦发生异常页面总会是定制的错误页,而不是这个函数的返回值。需要运行在debug=False的情况才生效,而且teardown_request是运行在after_request之后的。
before系列的钩子如果有return,那么视图函数就不会被执行。after系列钩子必须有return,这是最终返回给浏览器的内容
from flask import Flask, request
app = Flask(__name__)
@app.route('/index')
def index():
print('执行视图函数')
return 'index'
@app.route('/login')
def login():
print('执行视图函数')
return 'login'
@app.before_first_request
def hanlde_before_first_request():
print('before_first_request')
@app.before_request
def handel_before_request():
print('before_request')
@app.after_request
def handle_after_request(response):
print('handle_after_request')
if request.path == '/login':
print('login')
if request.path == '/index':
print('index')
return response
@app.teardown_request
def handle_teardown_request(response):
print('teardown_request')
return 'hahah'
if __name__ == '__main__':
app.run()
有了钩子,一次完成的http请求的后台处理就不单纯是视图函数了,一次完整的请求流程还包括钩子函数
Flask-Script扩展命令行
安装:pip3 install Flask-Script
from flask import Flask, request
from flask_script import Manager
app = Flask(__name__)
# 创建一个管理者用来管理app
manager = Manager(app)
@app.route('/index')
def index():
print('执行视图函数')
return 'index'
if __name__ == '__main__':
manager.run()
执行脚本python flask_test.py
的结果为
usage: flask_test.py [-?] {shell,runserver} ...
positional arguments:
{shell,runserver}
shell Runs a Python shell inside Flask application context.
runserver Runs the Flask development server i.e. app.run()
optional arguments:
-?, --help show this help message and exit
所以用了flask_script,原先的脚本就不是单纯可直接执行的脚本了,而是需要接受命令并按照命令去执行flask程序。Manager默认只提供了两条命令:shell 和 runserver,我们可以对其进行扩展。
数据库迁移
在开发过程中,需要修改数据库模型,而且还要在修改之后更新数据库。最直接的方式就是删除旧表,但这样会丢失数据。更好的解决办法是使用数据库迁移框架,它可以追踪数据库模式的变化,然后把变动应用到数据库中。
在Flask中可以使用Flask-Migrate扩展,来实现数据迁移。并且集成到Flask-Script中,所有操作通过命令就能完成。为了导出数据库迁移命令,Flask-Migrate提供了一个MigrateCommand类,可以附加到flask-script的manager对象上。
安装Flask-Migrate:pip3 install flask-migrate
from flask_script import Manager
from flask import Flask
from flask_migrate import Migrate,MigrateCommand
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
manager = Manager(app)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:mysql@127.0.0.1:3306/test'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
db = SQLAlchemy(app)
# 绑定数据库和app
Migrate(app, db)
# 增加一条管理命令
manager.add_command('db', MigrateCommand)
@app.route('/index')
def index():
return 'index'
#定义模型Role
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:'.format(self.name)
#定义用户
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:'.format(self.username)
if __name__ == '__main__':
manager.run()
- python database.py db init -- 这个命令会创建migrations文件夹,所有迁移文件都放在里面。
- python database.py db migrate -m 'initial migration' -- 创建修改版本,-m后面的是本次更改的提示信息
- python database.py db upgrade -- 向前更新数据库
- python database.py db history -- 查看历史版本的具体版本号
- python database.py db downgrade 版本号 -- 版本回退
jinja2模板
现在越来越多都是前后端分离,平常使用模板方式进行开发的场景越来越少。基本了解即可。
变量
flask_test.py
from flask import Flask,render_template
app = Flask(__name__)
@app.route('/')
def index():
mydict = {'key':'silence is gold'}
mylist = ['Speech', 'is','silver']
myintvar = 0
return render_template('vars.html',
mydict=mydict,
mylist=mylist,
myintvar=myintvar
)
if __name__ == '__main__':
app.run(debug=True)
vars.html
<p>{{mydict['key']}}</p>
<p>{{mydict.key}}</p>
<p>{{mylist[1]}}</p>
<p>{{mylist[myvariable]}}</p>
过滤器
safe:禁用转义;
<p>{{ '<em>hello</em>' | safe }}</p>
capitalize:把变量值的首字母转成大写,其余字母转小写;
<p>{{ 'hello' | capitalize }}</p>
lower:把值转成小写;
<p>{{ 'HELLO' | lower }}</p>
upper:把值转成大写;
<p>{{ 'hello' | upper }}</p>
title:把值中的每个单词的首字母都转成大写;
<p>{{ 'hello' | title }}</p>
trim:把值的首尾空格去掉;
<p>{{ ' hello world ' | trim }}</p>
reverse:字符串反转;
<p>{{ 'olleh' | reverse }}</p>
format:格式化输出;
<p>{{ '%s is %d' | format('name',17) }}</p>
striptags:渲染之前把值中所有的HTML标签都删掉;
<p>{{ '<em>hello</em>' | striptags }}</p>
对于xss攻击在后端也可以做:v = Markup("<input type='text' />")
支持链式使用过滤器
<p>{{ “ hello world “ | trim | upper }}</p>
列表过滤器
first:取第一个元素
<p>{{ [1,2,3,4,5,6] | first }}</p>
last:取最后一个元素
<p>{{ [1,2,3,4,5,6] | last }}</p>
length:获取列表长度
<p>{{ [1,2,3,4,5,6] | length }}</p>
sum:列表求和
<p>{{ [1,2,3,4,5,6] | sum }}</p>
sort:列表排序
<p>{{ [6,2,3,1,5,4] | sort }}</p>
自定义
@app.template_filter()
def add(x, y, z):
return x + y + z
@app.template_global()
def sub(x, y):
return x - y
调用方式是{{ 1|add(2,3)}}
和 {{ sub(3,2) }}
。和django不同,flask的filter可以接受不止两个参数。template_filter和template_global后面都需要加括号
宏
macro.html
{% macro input(name,value='',type='text',size=20) %}
<input type="{{ type }}"
name="{{ name }}"
value="{{ value }}"
size="{{ size }}"/>
{% endmacro %}
在其它模板文件中先导入,再调用
{% import 'macro.html' as func %}
{% func.input('name', 10) %}
request和url_for可以在前端模版直接使用,相当于全局变量
表单
使用Flask-WTF表单扩展,可以帮助进行CSRF验证,帮助我们快速定义表单模板,而且可以帮助我们在视图中验证表的数据。说白了,其角色就相当于是django的forms表单验证。
flask_test.py
from flask import Flask, render_template, request, redirect
from wtforms import Form
from wtforms.fields import core
from wtforms.fields import html5
from wtforms.fields import simple
from wtforms import validators
from wtforms import widgets
app = Flask(__name__, template_folder='templates')
app.debug = True
class RegisterForm(Form):
name = simple.StringField(
label='用户名',
validators=[
validators.DataRequired()
],
widget=widgets.TextInput(),
render_kw={'class': 'form-control'},
default='alex'
)
pwd = simple.PasswordField(
label='密码',
validators=[
validators.DataRequired(message='密码不能为空.')
],
widget=widgets.PasswordInput(),
render_kw={'class': 'form-control'}
)
pwd_confirm = simple.PasswordField(
label='重复密码',
validators=[
validators.DataRequired(message='重复密码不能为空.'),
validators.EqualTo('pwd', message="两次密码输入不一致")
],
widget=widgets.PasswordInput(),
render_kw={'class': 'form-control'}
)
email = html5.EmailField(
label='邮箱',
validators=[
validators.DataRequired(message='邮箱不能为空.'),
validators.Email(message='邮箱格式错误')
],
widget=widgets.TextInput(input_type='email'),
render_kw={'class': 'form-control'}
)
gender = core.RadioField(
label='性别',
choices=(
(1, '男'),
(2, '女'),
),
coerce=int
)
city = core.SelectField(
label='城市',
choices=(
('bj', '北京'),
('sh', '上海'),
)
)
hobby = core.SelectMultipleField(
label='爱好',
choices=(
(1, '篮球'),
(2, '足球'),
),
coerce=int
)
favor = core.SelectMultipleField(
label='喜好',
choices=(
(1, '篮球'),
(2, '足球'),
),
widget=widgets.ListWidget(prefix_label=False),
option_widget=widgets.CheckboxInput(),
coerce=int,
default=[1, 2]
)
def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
self.favor.choices = ((1, '篮球'), (2, '足球'), (3, '羽毛球'))
def validate_pwd_confirm(self, field):
"""
自定义pwd_confirm字段规则,例:与pwd字段是否一致
:param field:
:return:
"""
# 最开始初始化时,self.data中已经有所有的值
if field.data != self.data['pwd']:
# raise validators.ValidationError("密码不一致") # 继续后续验证
raise validators.StopValidation("密码不一致") # 不再继续后续验证
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'GET':
form = RegisterForm(data={'gender': 1})
return render_template('register.html', form=form)
else:
form = RegisterForm(formdata=request.form)
if form.validate():
print('用户提交数据通过格式验证,提交的值为:', form.data)
else:
print(form.errors)
return render_template('register.html', form=form)
if __name__ == '__main__':
app.run()
register.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>用户注册</h1>
<form method="post" novalidate style="padding:0 50px">
{% for item in form %}
<p>{{item.label}}: {{item}} {{item.errors[0] }}</p>
{% endfor %}
<input type="submit" value="提交">
</form>
</body>
</html>
蓝图
在没有蓝图之前,我们考虑一个问题:django的目录都是有一定规范的,flask的代码都在一个py文件里面,这肯定不能用于生产。所以,我们的想法是把一个py文件的代码分开,有些代码写到别的py文件,这样看起来也更加清晰。最直白的就是,我们把处理不同业务逻辑的视图放到不同的py文件中,比如处理订单相关的放到一个文件,处理用户相关的放到另外一个文件等等。那么我们可以这样做
flask_test.py
from flask import Flask
# 引入order
from order import order
app = Flask(__name__)
@app.route('/index')
def index():
return 'index'
if __name__ == '__main__':
print(app.url_map)
app.run()
order.py
from flask_test import app
@app.route('/order')
def order():
return 'order'
print('in order')
运行flask_test.py出错:
Traceback (most recent call last):
File "C:/Users/Administrator/Desktop/flask_test/flask_test.py", line 4, in <module>
from order import order
File "C:UsersAdministratorDesktopflask_testorder.py", line 4, in <module>
from flask_test import app
File "C:UsersAdministratorDesktopflask_testflask_test.py", line 4, in <module>
from order import order
ImportError: cannot import name 'order'
解决办法之一是延迟导入
解决办法之二是在order.py里面只定义函数,在flask_test.py对函数绑定路由
解决办法之三就是蓝图,也是推荐的方式。
在flask应用程序中国,app就类似于django的工程,蓝图就类似于django的app。
蓝图是保存了一组将来可以在应用对象上执行的操作。注册路由就是一种操作,当在程序实例上调用route装饰器注册路由时,这个操作将修改对象的url_map路由映射列表。当我们在蓝图对象上调用route装饰器注册路由时,它只是在内部的一个延迟操作记录列表defered_functions中添加了一个项。当执行应用对象的 register_blueprint() 方法时,应用对象从蓝图对象的 defered_functions 列表中取出每一项,即调用应用对象的 add_url_rule() 方法,这将会修改程序实例的路由映射列表。
flask_test.py
from flask import Flask
from order import order
app = Flask(__name__)
app.register_blueprint(order, url_prefix='/order')
@app.route('/index')
def index():
return 'index'
if __name__ == '__main__':
print(app.url_map)
app.run()
order.py
from flask import Blueprint
# 'order' 是蓝图的名字,体现在url_map上,静态目录和模版目录都需要手动指定
# 和app不同,蓝图的模版和静态目录都没有默认值,,找模版文件先从app目录找,随后到蓝图目录找
order = Blueprint('order', __name__, template_folder='templates', static_folder='static')
@order.route('/get_order')
def get_order():
return 'get order'
单元测试
介绍
在Web开发过程中,单元测试实际上就是一些“断言”(assert)代码。
断言就是判断一个函数或对象的一个方法所产生的结果是否符合你期望的那个结果。 python中assert断言是声明布尔值为真的判定,如果表达式为假会发生异常。单元测试中,一般使用assert来断言结果。
常用的断言方法
assertEqual 如果两个值相等,则pass
assertNotEqual 如果两个值不相等,则pass
assertTrue 判断bool值为True,则pass
assertFalse 判断bool值为False,则pass
assertIsNone 不存在,则pass
assertIsNotNone 存在,则pass
基本使用
import unittest
class TestClass(unittest.TestCase):
#该方法会首先执行,相当于做测试前的准备工作
def setUp(self):
pass
#该方法会在测试代码执行完后执行,相当于做测试后的扫尾工作
def tearDown(self):
pass
#测试代码,必须以test开头
def test_xxx(self):
pass
# coding:utf-8
import unittest
from login import app
import json
class TestLogin(unittest.TestCase):
"""定义测试案例"""
def setUp(self):
"""在执行具体的测试方法前,先被调用"""
# 可以使用python的http标准客户端进行测试
# urllib urllib2 requests
# 开启测试模式,这样flask程序报的错就会完全在测试程序显示出来,而不是显示测试程序本身的错误
app.config['TESTING'] = True
# 使用flask提供的测试客户端进行测试
self.client = app.test_client()
def test_empty_name_password(self):
"""测试模拟场景,用户名或密码不完整"""
# 使用客户端向后端发送post请求, data指明发送的数据,会返回一个响应对象
response = self.client.post("/login", data={})
# respoonse.data是响应体数据
resp_json = response.data
# 按照json解析
resp_dict = json.loads(resp_json)
# 使用断言进行验证
self.assertIn("code", resp_dict)
code = resp_dict.get("code")
self.assertEqual(code, 1)
# 测试只传name
response = self.client.post("/login", data={"name": "admin"})
# respoonse.data是响应体数据
resp_json = response.data
# 按照json解析
resp_dict = json.loads(resp_json)
# 使用断言进行验证
self.assertIn("code", resp_dict)
code = resp_dict.get("code")
self.assertEqual(code, 1)
def test_wrong_name_password(self):
"""测试用户名或密码错误"""
# 使用客户端向后端发送post请求, data指明发送的数据,会返回一个响应对象
response = self.client.post("/login", data={"name": "admin", "password": "itcast"})
# respoonse.data是响应体数据
resp_json = response.data
# 按照json解析
resp_dict = json.loads(resp_json)
# 使用断言进行验证
self.assertIn("code", resp_dict)
code = resp_dict.get("code")
self.assertEqual(code, 2)
if __name__ == '__main__':
unittest.main()
login.py
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/login", methods=["POST"])
def login():
"""登录"""
name = request.form.get("name")
password = request.form.get("password")
# "" 0 [] () {} None 在逻辑判断时都是假
if not all([name, password]):
# 表示name或password中有一个为空或者都为空
return jsonify(code=1, message=u"参数不完整")
if name == "admin" and password =="python":
return jsonify(code=0, message=u"OK")
else:
return jsonify(code=2, message=u"用户名或密码错误")
if __name__ == '__main__':
app.run()
部署
当我们执行下面的hello.py时,使用的flask自带的服务器,完成了web服务的启动。在生产环境中,flask自带的服务器,无法满足性能要求,我们这里采用Gunicorn做wsgi容器,来部署flask程序。Gunicorn(绿色独角兽)是一个Python WSGI的HTTP服务器。从Ruby的独角兽(Unicorn )项目移植。该Gunicorn服务器与各种Web框架兼容,实现非常简单,轻量级的资源消耗。Gunicorn直接用命令启动,不需要编写配置文件,相对uWSGI要容易很多。
web开发中,部署方式大致类似。简单来说,前端代理使用Nginx主要是为了实现分流、转发、负载均衡,以及分担服务器的压力。Nginx部署简单,内存消耗少,成本低。Nginx既可以做正向代理,也可以做反向代理。
正向代理:请求经过代理服务器从局域网发出,然后到达互联网上的服务器。
特点:服务端并不知道真正的客户端是谁。
反向代理:请求从互联网发出,先进入代理服务器,再转发给局域网内的服务器。
特点:客户端并不知道真正的服务端是谁。
区别:正向代理的对象是客户端。反向代理的对象是服务端。
一般部署的时候不是用一台服务器进行部署,业务服务器比如说有两台,mysql服务器有一台,redis服务器有一台,其中业务服务器部署看gunicorn和flask程序,nginx就会把请求均衡地分发到这两台部署环境一样的服务器,并且nginx设计之初就是为了提供静态文件的支持,所以让nginx帮忙处理静态资源。
安装gunicorn: pip3 install gunicorn
启动:gunicorn -w 4 -b 127.0.0.1:5001 -D --access_log='/tmp/log' 运行文件名称:Flask程序实例名
此时共有5个进程,一个父进程,一个子进程
upstream flask {
server 10.20.1.11:5000;
server 10.20.1.12:6000;
}
server {
# 监听80端口
listen 80;
# 本机
server_name localhost;
# 默认请求的url
location / {
#请求转发到gunicorn服务器
proxy_pass http://flask;
#设置请求头,并将头信息传递给服务器端
proxy_set_header Host $host;
}
}