Flask 综合认证 上下文 蓝图 请求钩子

请求钩子

请求钩子可以对请求的各阶段进行监听, 方便开发者 针对请求完成一些统一的处理, 以便减少重复代码, 作用类比Django中的中间件

开发中中主要会用到以下四种请求钩子:
before_request

  • 每次执行视图函数之前调用
  • 对请求进行一些准备处理
  • 如果在该函数中返回了一个响应,视图函数将不再被调用

after_request

  • 如果没有抛出错误,每次执行视图函数之后(已经包装为响应对象)调用
  • 在此函数中可以对响应值在返回之前做最后一步修改处理
  • 接受一个参数:包装好的响应对象
  • 需要将修改后的响应对象返回

before_first_reques

  • web应用被第一次请求前调用
  • 可以进行web应用初始化处理

teardown_request:

  • 每次执行视图函数之后调用
  • 无论是否出现异常都会执行, 一般用于请求收尾
  • 接受一个参数:错误信息,如果有相关错误抛出
    Flask 综合认证 上下文 蓝图 请求钩子_第1张图片
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)

Flask 综合认证 上下文 蓝图 请求钩子_第2张图片

使用细节

蓝图的三个使用细节

  • 创建蓝图时, 可以通过 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 综合认证 上下文 蓝图 请求钩子_第3张图片

切换配置

  • 虽然封装了多套配置, 但需要 修改代码才能切换配置, 这种方式并不利于开发和测试
  • 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()

Flask 综合认证 上下文 蓝图 请求钩子_第4张图片

动态创建应用

  • 通过代码调用函数 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()来定义路由, 但是 蓝图定义路由仍然有效

Flask 综合认证 上下文 蓝图 请求钩子_第5张图片

你可能感兴趣的:(Flask,flask,python)