请求钩子
请求钩子可以对请求的各阶段进行监听, 方便开发者 针对请求完成一些统一的处理, 以便减少重复代码, 作用类比Django中的中间件
开发中中主要会用到以下四种请求钩子:
before_request
- 每次执行视图函数之前调用
- 对请求进行一些准备处理
- 如果在该函数中返回了一个响应,视图函数将不再被调用
after_request
- 如果没有抛出错误,每次执行视图函数之后(已经包装为响应对象)调用
- 在此函数中可以对响应值在返回之前做最后一步修改处理
- 接受一个参数:包装好的响应对象
- 需要将修改后的响应对象返回
before_first_reques
- web应用被第一次请求前调用
- 可以进行web应用初始化处理
teardown_request:
- 每次执行视图函数之后调用
- 无论是否出现异常都会执行, 一般用于请求收尾
- 接受一个参数:错误信息,如果有相关错误抛出
from flask import Flask, Response
# 请求钩子: 类比django中间件 ,请求钩子可以对请求的各阶段进行监听, 方便开发者针对请求完成一些统一的处理
app = Flask(__name__)
# 每次执行视图函数之前调用, 对请求进行一些准备处理, 如参数解析, 黑名单过滤, 数据统计等
@app.before_request
def prepare():
print('before_request')
# 每次执行视图函数之后(已经包装为响应对象)调用, 对响应进行一些加工处理, 如设置统一响应头, 设置数据的外层包装
@app.after_request
def process(response:Response): # 必须定义形参接收响应对象
print('after_request:')
# print(response.headers)
# print(response.data)
# print(response.status_code)
return response
# web应用被第一次请求前调用, 可以进行web应用初始化处理, 如数据库连接
@app.before_first_request
def initial():
print('before_first_request')
# 每次执行视图函数之后调用, 无论是否出现异常都会执行, 一般用于请求收尾, 如资源回收, 异常统计
@app.teardown_request # 测试时不要开启调试模式
def request_handle(error): # 必须定义形参来接收具体错误信息, 如果没有错误, error=None
print('teardown_request : %s' % error)
@app.route('/')
def index():
print('执行视图')
a = 1 / 0
return "index"
if __name__ == '__main__':
app.run()
- 请求钩子可以通过两种方式进行添加, 装饰器形式 或者 直接方法调用
from flask import Flask, Response
app = Flask(__name__)
# 每次执行视图函数之前调用
# @app.before_request
# def prepare():
# print('before_request')
# 另一种语法
def prepare():
print('before_request')
app.before_request(prepare)
@app.route('/')
def index():
print('执行视图')
a = 1 / 0
return "index"
if __name__ == '__main__':
app.run()
蓝图
- 蓝图的作用: 实现Flask项目 模块化
- 项目模块化主要是 将业务以功能模块进行划分, 每个功能模块对应一个包, 用于存放和其有关的视图/工具/模型文件等, 如home, user
- 对于大型项目, 一般 每个功能模块对应创建一个蓝图, 由多个蓝图代替应用来分别管理各模块的视图
--------- project # 工程目录
|------ main.py # 启动文件
|------ user # 用户模块
| |— init.py # 包的初始化文件, 此处创建管理用户模块的蓝图对象
| |— views.py # 视图文件
| |— …
|
|------ home # 首页模块
| |— init.py # 包的初始化文件, 此处创建管理首页模块的蓝图对象
| |— views.py # 视图文件
| |— …
|…
在 home包 的初始化文件 init.py 中, 创建蓝图对象
# home/__init__.py
from flask import Blueprint
# 1. 创建蓝图对象
home_blu = Blueprint("home_b", __name__)
在 home包中创建 views文件, 存放视图函数
定义视图函数时, 使用蓝图对象来定义路由.
# home/views.py
from home import home_blu
# 2. 使用蓝图对象来定义路由
@home_blu.route('/')
def index():
return "index"
想要让蓝图对象能够完成路由定义, 还需要 Flask应用注册蓝图对象
# main.py
from flask import Flask
from home import home_blu
app = Flask(__name__)
# 3.应用注册蓝图对象
app.register_blueprint(home_blu)
if __name__ == '__main__':
print(app.url_map)
app.run(debug=True)
使用细节
蓝图的三个使用细节
- 创建蓝图时, 可以通过 url_prefix参数 给蓝图定义的路由添加 统一的URL资源段前缀
- 蓝图定义的路由, 其函数标记为 蓝图名.函数
- 蓝图也可以 设置请求钩子
只有访问该蓝图定义的路由时才会触发
实现局部监听
# home/__init__.py
from flask import Blueprint
# 细节1: 可以通过url_prefix参数给蓝图定义的路由添加统一的URL资源段前缀
home_blu = Blueprint("home_b", __name__, url_prefix='/home')
# 细节3: 蓝图也可以设置请求钩子 只有访问该蓝图定义的路由时才会触发 局部监听
@home_blu.before_request
def home_prepare():
print('home_prepare')
from . import views
# home/views.py
from flask import url_for
from home import home_blu
@home_blu.route('/')
def index():
return "index"
@home_blu.route('/demo1')
def demo1():
# 细节2: 蓝图定义的路由, 其函数标记为 蓝图名.函数名
url1 = url_for('home_b.demo1')
# print(url1)
return 'demo1'
上下文 (context)
- 上下文:是一个 数据容器,保存了 Flask 程序运行过程中的一些信息。
- Flask中有两种上下文,请求上下文 和 应用上下文
- 两种上下文的使用范围相同, 从请求开始到请求结束, 在范围外使用会报错
1. 请求上下文
- 记录一些和请求有关的数据, 包括request和session两个变量
request
- 封装了HTTP请求的内容,针对的是http请求。
session
- 用来记录请求会话中的信息,针对的是用户信息。
2. 应用上下文
记录一些和应用有关的数据, 包括current_app和g两个变量
current_app
- 会自动引用创建的Flask对象, 需要在项目的其他文件中使用app时, 应该通过current_app来获取, 可以减少循环导入问题
g
- flask给开发者预留的一个容器, 用于记录自定义数据
- g变量每次请求会重置数据
- g使用场景: 1> 在钩子函数和视图函数之间传递数据 2> 函数嵌套调用时传递数据
# main.py
from flask import Flask, request, current_app, g
# 上下文变量: 有使用范围 [请求开始, 请求结束]
# 请求上下文: 记录一些和请求有关的数据 request session
# 应用上下文: 记录一些和应用有关的数据 current_app g
import tool
app = Flask(__name__)
@app.route('/')
def index():
# print(request.url)
g.name = 'zs'
tool.func1()
return "index"
@app.route('/demo1')
def demo1():
print(g.name) # 会报错
return 'demo1'
if __name__ == '__main__':
# print(request.url) # 使用范围外, 会报错
app.run(debug=True)
如果是多层函数嵌套,可以使用g变量因为在一个请求内,如果每层嵌套都传参的话会很麻烦
# tool.py
from flask import g, current_app
def func1():
print(g.name)
print(current_app.url_map)
# tool2.func2()
上下文机制 (理解)
问题1: 上下文变量是否为全局变量?
- 不是全局变量, web服务器会通过多线程并发调用web应用, 而全局变量会被所有线程共享, 无法记录并发的多个请求数据
- 上下文机制实现了线程隔离(LocalStack类型, 本质是字典, key是线程id, 值是上下文变量), 每个线程存取自己的数据, 相互不影响
- 请求1 -> 线程1 -> request = 请求1
- 请求2 -> 线程2 -> request = 请求2
问题2: 上下文为什么设置使用范围?
- 主要目的为节省内存
- 请求开始时, 创建上下文(记录上下文变量);
- 请求结束时, 销毁上下文(将上下文变量删除, 数据占用的空间被释放)
综合认证
1. 统一处理
- 需求: 获取用户身份
- 分析: 除了静态资源, 基本所有视图都需要获取用户身份, 每个视图单独获取出现大量的代码冗余
- 解决办法: 设置 请求钩子, 并通过 g变量 将数据传递给视图函数
from flask import Flask, session, g
app = Flask(__name__)
app.secret_key = 'test'
# 需求1: 所有视图都需要获取用户身份
# 解决办法: 用钩子函数进行封装 减少代码冗余
@app.before_request
def prepare():
# 必须使用g变量来传递数据, 使用全局变量不能记录并发的多个请求数据
g.name = session.get('username')
@app.route('/')
def index():
if g.name:
return "欢迎回来, %s" % g.name
else:
return '首页'
@app.route('/demo1/')
def demo1():
print(g.name)
return 'demo1'
@app.route('/login')
def login():
"""登录"""
session['username'] = 'zs'
return '登录成功'
if __name__ == '__main__':
app.run(debug=True)
2. 访问限制
- 需求: 对指定的路由进行访问限制
- 分析: 部分视图需要身份校验, 这部分视图每个单独校验仍会出现大量的代码冗余
- 解决办法: 封装 装饰器 完成身份校验逻辑, 对指定视图函数设置装饰器
from flask import Flask, session, g, abort
from functools import wraps
app = Flask(__name__)
app.secret_key = 'test'
@app.before_request
def prepare():
g.name = session.get('username')
@app.route('/')
def index():
if g.name:
return "欢迎回来, %s" % g.name
else:
return '首页'
@app.route('/login')
def login():
"""登录"""
session['username'] = 'zs'
return '登录成功'
# 需求2: 对部分视图进行访问限制 如个人中心必须登录才能访问
# 解决方案: 使用装饰器封装访问限制 减少代码冗余
def login_required(f): # f = user
def wrapper(*args, **kwargs):
# 获取函数名
print(wrapper.__name__)
if g.name: # 用户已登录
return f(*args, **kwargs) # 正常访问视图函数
else: # 用户未登录
abort(401) # 400 语法/参数错误 401 未认证 403 已认证, 权限不足 404 资源不存在 405 请求方式不支持 500 服务器错误
return wrapper
@app.route('/user')
@login_required # user = login_required(user)
def user():
"""个人中心"""
return '访问 %s 的个人中心' % g.name
if __name__ == '__main__':
print(app.url_map)
app.run(debug=True)
functools.wraps
- 系统内置的装饰器, 主要用于装饰器中的闭包函数
- 作用是 将被装饰的函数(wrapper)的函数信息 替换为 指定函数(f)的函数信息 (包括name 函数名, doc 函数注释等)
未设置wraps装饰器的情况:
def decorater1(f): # f = demo
def wrapper(*args, **kwargs):
# 获取函数名
print(wrapper.__name__)
return f(*args, **kwargs)
return wrapper
@decorater1
def demo():
print("哈哈")
# 调用函数
demo()
# 运行结果
wrapper # 打印闭包函数名
哈哈
设置wraps装饰器的情况:
from functools import wraps
def decorater1(f): # f = demo
@wraps(f) # 设置装饰器
def wrapper(*args, **kwargs):
# 获取函数名
print(wrapper.__name__)
return f(*args, **kwargs)
return wrapper
@decorater1
def demo():
print("哈哈")
# 调用函数
demo()
# 运行结果
demo # 打印原函数名
哈哈
- flask中的函数标记是根据函数名生成的
- 视图函数添加装饰器login_required 后, 函数标记都会使用闭包函数名wrapper, 这样会出现函数标记冲突, 程序报错
- 解决办法: 给闭包函数添加装饰器functools.wraps(f), 让函数标记使用原视图函数名生成
from flask import Flask, session, g, abort
from functools import wraps
app = Flask(__name__)
app.secret_key = 'test'
@app.before_request
def prepare():
g.name = session.get('username')
@app.route('/')
def index():
if g.name:
return "欢迎回来, %s" % g.name
else:
return '首页'
@app.route('/login')
def login():
"""登录"""
session['username'] = 'zs'
return '登录成功'
# 使用装饰器封装访问限制
def login_required(f): # f = user
@wraps(f) # 会将被装饰的函数(wrapper)的函数信息替换为指定函数(f)的函数信息(__name__ 函数名, __doc__ 函数注释)
# 设置该装饰器后, 可以让闭包函数使用原函数名, 避免函数标记出现冲突(函数标记是根据函数名来生成的)
def wrapper(*args, **kwargs):
if g.name:
return f(*args, **kwargs)
else:
abort(401)
return wrapper
@app.route('/user')
@login_required
def user():
"""个人中心"""
return '访问 %s 的个人中心' % g.name
@app.route('/demo1')
@login_required
def demo1():
return 'demo1'
if __name__ == '__main__':
print(app.url_map)
app.run(debug=True)
应用配置
1. 加载配置
- app.config 用于设置配置, 该属性继承自 dict, 可以以字典形式赋值取值
rom datetime import timedelta
from flask import Flask, session
app = Flask(__name__)
app.secret_key = 'test'
# config属性用于设置配置
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
@app.route('/')
def index():
# 设置session 用于测试配置是否生效
session['name'] = 'zs'
# 读取配置
print(app.config.get('PERMANENT_SESSION_LIFETIME'))
return "index"
if __name__ == '__main__':
app.run(debug=True)
- 实际开发中, 应该将配置封装起来, 方便进行统一的管理, 类似Django的settings文件
- Flask提供了多种封装方式, 这里先介绍一种常用的方案 – 从对象中加载配置
从对象中加载配置
实际开发中, 项目往往存在多套配置
- 开发环境配置
- 生产环境配置
- 测试环境配置
各种环境的配置既有相同也有不同, 大量相同的配置会出现代码冗余
从对象中加载配置
配置封装方案之一, 以面向对象的形式 封装配置, 有利于 减少重复代码 以及 代码解耦合
定义配置文件 config.py, 在文件中将应用配置 以类的形式 封装起来
config.py
from datetime import timedelta
class BaseConfig:
"""配置基类 可以将相同的配置抽取到基类中, 减少重复代码"""
# 定义和配置同名的类属性
PERMANENT_SESSION_LIFETIME = timedelta(days=7)
class DevelopmentConfig(BaseConfig):
"""开发环境"""
SQL_URL = '127.0.0.1:3306/test1' # 数据库地址
class ProductionConfig(BaseConfig):
"""生产环境"""
SQL_URL = '222.10.15:3306/users' # 数据库地址
主文件 main.py 从对象中加载封装的配app.config.from_object()
main.py
from datetime import timedelta
from flask import Flask
app = Flask(__name__)
# 从对象中加载配置
# 优点: 面向对象的设计有利于 减少重复代码 以及 代码解耦合
from config import DevelopmentConfig
app.config.from_object(DevelopmentConfig)
@app.route('/')
def index():
print(app.config.get('PERMANENT_SESSION_LIFETIME'))
return "index"
if __name__ == '__main__':
app.run(debug=True)
切换配置
- 虽然封装了多套配置, 但需要 修改代码才能切换配置, 这种方式并不利于开发和测试
- Flask提供了切换配置的更好方案, 需要进行以下两步
定义工厂函数, 封装应用的创建过程
利用环境变量, 调用工厂函数, 指定配置并动态创建应用
定义工厂函数
定义工厂函数, 封装应用的创建过程
# config.py
from datetime import timedelta
class BaseConfig:
"""配置基类 可以将相同的配置抽取到基类中, 减少重复代码"""
# 定义和配置同名的类属性
PERMANENT_SESSION_LIFETIME = timedelta(days=7)
class DevelopmentConfig(BaseConfig):
"""开发环境"""
SQL_URL = '127.0.0.1:3306/test1' # 数据库地址
class ProductionConfig(BaseConfig):
"""生产环境"""
SQL_URL = '222.10.15:3306/users' # 数据库地址
# 定义字典来记录 配置类型 和 配置子类 之间的映射关系
config_dict = {
'dev': DevelopmentConfig,
'pro': ProductionConfig
}
在主文件 main.py 中, 定义工厂函数封装应用的创建过程, 并通过参数指定应用对应的配置类型
工厂函数:根据需求在函数的内部封装对象的创建过程
from flask import Flask, current_app, Config
from config import config_dict
# 工厂函数: 根据参数需求, 内部封装对象的创建过程
def create_app(config_type):
"""封装应用的创建过程"""
# 创建应用
flask_app = Flask(__name__)
# 根据配置类型取出对应的配置子类
config_class = config_dict[config_type]
# 加载普通配置
flask_app.config.from_object(config_class)
return flask_app
# 创建应用对象
app = create_app('dev')
@app.route("/")
def index():
print(app.config.get('SQL_URL'))
return "index"
if __name__ == '__main__':
app.run()
动态创建应用
- 通过代码调用函数 create_app(‘dev’) 创建应用, 仍然在代码中写死了 配置类型dev
- 通过代码调用函数 create_app(‘dev’) 创建应用, 仍然在代码中写死了 配置类型dev
格式: FLASK_APP=“应用所在模块:工厂函数(参数)”
$ export FLASK_APP="main:create_app('dev')" # 设置环境变量, 通过调用函数 设置配置并动态创建Flask应用
$ flask run # 运行web程序
- 注意: 如果动态创建应用, 通过 app = create_app(‘dev’) 代码生成的Flask应用将会失效
- 需要使用 app 的时候, 可以使用 current_app来代替 (只能在请求范围中使用)
- 将无法通过 app.route()来定义路由, 但是 蓝图定义路由仍然有效