flask从入门到精通之钩子、异常、context、jinjia模板、过滤器

发布时间 2023-09-15 15:25:08作者: 异步非阻塞

一、请求全局钩子【hook】

此处的全局钩子,其实就是类似django里面的中间件。 也就是只要调用或者注册了,在http请求响应中是必然执行的。

在客户端和服务器交互的过程中,有些准备工作或扫尾工作需要处理,比如:

  • 在项目运行开始时,建立数据库连接,或创建连接池;

  • 在客户端请求开始时,根据需求进行身份识别,权限校验;

  • 在请求结束视图返回数据时,指定转换数据的格式,或者记录操作日志;

为了让每个视图函数避免编写重复功能的代码,Flask提供了通用设置的功能,即请求钩子。

请求钩子是通过装饰器的形式实现,Flask支持如下四种请求钩子(注意:钩子的装饰器名字是固定):

  • before_first_request

    • 在处理第一个请求前执行[项目刚运行第一次被客户端请求时执行的钩子]

  • before_request

    • 在每一次请求前执行[项目运行后,每一次接收到客户端的request请求都会执行一次]

    • 如果在某修饰的函数中返回了一个响应,视图函数将不再被调用

  • after_request

    • 如果没有抛出错误,在每次请求后执行

    • 接受一个参数:视图函数作出的响应

    • 在此函数中可以对响应值在返回之前做最后一步修改处理

    • 需要将参数中的响应在此参数中进行返回

  • teardown_request:

    • 在每一次请求后执行

    • 接受一个参数:错误信息,如果有相关错误抛出

    • 需要设置flask的配置DEBUG=False,teardown_request才会接受到异常对象。

代码

 1 from flask import Flask, session
 2 
 3 # 应用实例对象
 4 app = Flask(__name__)
 5 
 6 """给app单独设置配置项"""
 7 # 设置秘钥
 8 app.config["SECRET_KEY"] = "my SECRET KEY"
 9 
10 @app.before_first_request
11 def before_first_request():
12     """
13     这个钩子会在项目启动后第一次被用户访问时执行
14     可以编写一些初始化项目的代码,例如,数据库初始化,加载一些可以延后引入的全局配置
15     """
16     print("----before_first_request----")
17     print("系统初始化的时候,执行这个钩子方法")
18     print("会在接收到第一个客户端请求时,执行这里的代码")
19 
20 
21 @app.before_request
22 def before_request():
23     """
24     这个钩子会在每次客户端访问视图的时候执行
25     # 可以在请求之前进行用户的身份识别,以及对于本次访问的用户权限等进行判断。..
26     """
27     print("----before_request----")
28     print("每一次接收到客户端请求时,执行这个钩子方法")
29     print("一般可以用来判断权限,或者转换路由参数或者预处理客户端请求的数据")
30 
31 
32 @app.after_request
33 def after_request(response):
34     print("----after_request----")
35     print("在处理请求以后,执行这个钩子方法")
36     print("一般可以用于记录会员/管理员的操作历史,浏览历史,清理收尾的工作")
37 
38     response.headers["Content-Type"] = "application/json"
39     response.headers["Company"] = "python.Edu..."
40 
41     # 必须返回response参数
42     return response
43 
44 
45 @app.teardown_request
46 def teardown_request(exc):
47     print("----teardown_request----")
48     print("在每一次请求以后,执行这个钩子方法")
49     print("如果有异常错误,则会传递错误异常对象到当前方法的参数中")
50     # 在项目关闭了DEBUG模式以后,则异常信息就会被传递到exc中,我们可以记录异常信息到日志文件中
51     print(f"错误提示:{exc}")  # 异常提示
52 
53 
54 @app.route("/")
55 def index():
56     print("-----------视图函数执行了---------------")
57     return "ok"
58 
59 if __name__ == '__main__':
60     # 启动项目的web应用程序
61     app.run(host="0.0.0.0", port=5000, debug=False)

在第1次请求时的打印(关闭DEBUG模式,视图代码正确执行,没有异常的情况):

----before_first_request----
系统初始化的时候,执行这个钩子方法
会在接收到第一个客户端请求时,执行这里的代码
----before_request----
每一次接收到客户端请求时,执行这个钩子方法
一般可以用来判断权限,或者转换路由参数或者预处理客户端请求的数据
-----------视图函数执行了---------------
----after_request----
在处理请求以后,执行这个钩子方法
一般可以用于记录会员/管理员的操作历史,浏览历史,清理收尾的工作
----teardown_request----
在每一次请求以后,执行这个钩子方法
如果有异常错误,则会传递错误异常对象到当前方法的参数中
错误提示:None

在第2次请求时的打印(关闭DEBUG模式,视图代码正确执行,没有异常的情况):

----before_request----
每一次接收到客户端请求时,执行这个钩子方法
一般可以用来判断权限,或者转换路由参数或者预处理客户端请求的数据
-----------视图函数执行了---------------
----after_request----
在处理请求以后,执行这个钩子方法
一般可以用于记录会员/管理员的操作历史,浏览历史,清理收尾的工作
----teardown_request----
在每一次请求以后,执行这个钩子方法
如果有异常错误,则会传递错误异常对象到当前方法的参数中
错误提示:None

在第1次请求时的打印(关闭DEBUG模式,视图执行错误,有异常的情况):

----before_first_request----
系统初始化的时候,执行这个钩子方法
会在接收到第一个客户端请求时,执行这里的代码
----before_request----
每一次接收到客户端请求时,执行这个钩子方法
一般可以用来判断权限,或者转换路由参数或者预处理客户端请求的数据
-----------视图函数执行了---------------
----after_request----
在处理请求以后,执行这个钩子方法
一般可以用于记录会员/管理员的操作历史,浏览历史,清理收尾的工作
----teardown_request----
在每一次请求以后,执行这个钩子方法
如果有异常错误,则会传递错误异常对象到当前方法的参数中
错误提示:division by zero

在第2次请求时的打印(关闭DEBUG模式,视图执行有异常的情况):

----before_request----
每一次接收到客户端请求时,执行这个钩子方法
一般可以用来判断权限,或者转换路由参数或者预处理客户端请求的数据
-----------视图函数执行了---------------
----after_request----
在处理请求以后,执行这个钩子方法
一般可以用于记录会员/管理员的操作历史,浏览历史,清理收尾的工作
----teardown_request----
在每一次请求以后,执行这个钩子方法
如果有异常错误,则会传递错误异常对象到当前方法的参数中
错误提示:division by zero

钩子装饰器装饰了多个函数的执行顺序如下:

from flask import Flask, session

# 应用实例对象
app = Flask(__name__)

"""给app单独设置配置项"""
# 设置秘钥
app.config["SECRET_KEY"] = "my SECRET KEY"

@app.before_first_request
def before_first_request():
    """
    这个钩子会在项目启动后第一次被用户访问时执行
    可以编写一些初始化项目的代码,例如,数据库初始化,加载一些可以延后引入的全局配置
    """
    print("----before_first_request----")
    print("系统初始化的时候,执行这个钩子方法")
    print("会在接收到第一个客户端请求时,执行这里的代码")


@app.before_request
def before_request():
    """
    这个钩子会在每次客户端访问视图的时候执行
    # 可以在请求之前进行用户的身份识别,以及对于本次访问的用户权限等进行判断。..
    """
    print("----before_request----")
    print("每一次接收到客户端请求时,执行这个钩子方法")
    print("一般可以用来判断权限,或者转换路由参数或者预处理客户端请求的数据")


@app.after_request
def after_request1(response):
    print("----after_request1----")
    print("在处理请求以后,执行这个钩子方法")
    print("一般可以用于记录会员/管理员的操作历史,浏览历史,清理收尾的工作")

    response.headers["Content-Type"] = "application/json"
    response.headers["Company"] = "python oldboy..."

    # 必须返回response参数
    return response


@app.after_request
def after_request2(response):
    print("----after_request2----")

    # 必须返回response参数
    return response

@app.after_request
def after_request3(response):
    print("----after_request3----")

    # 必须返回response参数
    return response

@app.teardown_request
def teardown_request(exc):
    print("----teardown_request----")
    print("在每一次请求以后,执行这个钩子方法")
    print("如果有异常错误,则会传递错误异常对象到当前方法的参数中")
    # 在项目关闭了DEBUG模式以后,则异常信息就会被传递到exc中,我们可以记录异常信息到日志文件中
    print(f"错误提示:{exc}")  # 异常提示


@app.route("/")
def index():
    print("-----------视图函数执行了---------------")
    return "ok"

if __name__ == '__main__':
    # 启动项目的web应用程序
    app.run(host="0.0.0.0", port=5000, debug=False)

执行效果:

----before_first_request----
系统初始化的时候,执行这个钩子方法
会在接收到第一个客户端请求时,执行这里的代码
----before_request----
每一次接收到客户端请求时,执行这个钩子方法
一般可以用来判断权限,或者转换路由参数或者预处理客户端请求的数据
-----------视图函数执行了---------------
----after_request3----
----after_request2----
----after_request1----
在处理请求以后,执行这个钩子方法
一般可以用于记录会员/管理员的操作历史,浏览历史,清理收尾的工作
----teardown_request----
在每一次请求以后,执行这个钩子方法
如果有异常错误,则会传递错误异常对象到当前方法的参数中
错误提示:None

结论:以视图执行为中心点,before_request之前(请求过程)的先装饰先执行,after_request之后(响应过程),后装饰的先执行。

二、异常抛出和捕获异常

主动抛出http异常

  • abort 方法

    • 抛出一个给定状态代码的 HTTPException 或者 指定响应,例如想要用一个页面未找到异常来终止请求,你可以调用 abort(404)

  • 参数:

    • code – HTTP的错误状态码

from flask import Flask,abort,request
app = Flask(import_name=__name__)


# 配置类
class Config(object):
    DEBUG = True     # 开启调试模式

# 加载配置
app.config.from_object(Config)


@app.route("/")
def index():
    # try:
    #     1/0
    # except:
    #     abort(500)

    username = request.args.get("username")
    if username is None:
        abort(400)

    return "ok"

if __name__ == '__main__':
    app.run()

abort,只能抛出 HTTP 协议的错误状态码,一般用于权限等页面上错误的展示提示.

abort 在有些前后端分离的项目里面不会被使用,往往在业务错误的时候使用raise进行抛出错误类型,而不是抛出http异常。

捕获错误

  • app.errorhandler 装饰器

    • 注册一个错误处理程序,当程序抛出指定错误状态码的时候,就会调用该装饰器所装饰的方法

  • 参数:

    • code_or_exception – HTTP的错误状态码或指定异常

  • 例如统一处理状态码为500的错误给用户友好的提示:

@app.errorhandler(500)
def internal_server_error(e):
    return '服务器搬家了'

捕获指定异常类型

@app.errorhandler(ZeroDivisionError)
def zero_division_error(e):
    return '除数不能为0'

 

代码:

 1 from flask import Flask, request, abort
 2 
 3 # 应用实例对象
 4 app = Flask(__name__)
 5 
 6 class APIError(Exception):
 7     pass
 8 
 9 """异常抛出"""
10 @app.route("/")
11 def index():
12     username = request.args.get("username")
13     if username is None:
14         abort(400)
15     if username != "xiaoming":
16         raise APIError
17     return "ok"
18 
19 
20 @app.errorhandler(400)
21 def internal_server_error(e):
22     return {
23         "errno": 400,
24         "errmsg": "参数有误!",
25     }
26 
27 
28 @app.errorhandler(APIError)
29 def api_error(e):
30     return {
31         "errno": 500,
32         "errmsg": "接口访问有误!",
33     }
34 
35 
36 if __name__ == '__main__':
37     # 启动项目的web应用程序
38     app.run(host="0.0.0.0", port=5000, debug=True)

三、context

执行上下文:即语境,语意,在程序中可以理解为在代码执行到某一行时,根据之前代码所做的操作以及下文即将要执行的逻辑,可以决定在当前时刻下可以使用到的变量,或者可以完成的事情。

Flask中提供的执行上下文对象:相当于一个容器,保存了 Flask 程序运行过程中的一些信息[变量、函数、类与对象等信息]。

Flask中有两种上下文,请求上下文(request context)和应用上下文(application context)。

  1. application 指的就是当你调用app = Flask(__name__)创建的这个对象app

  2. request 指的是每次http请求发生时,WSGI server(比如gunicorn/uwsgi)调用Flask.__call__()之后,在Flask对象内部创建的Request对象;

  3. application 表示用于响应WSGI请求的应用本身,request 表示服务端每次响应客户单的http请求;

  4. application的生命周期大于request,一个application存活期间,可能发生多次http请求,所以也就会有多个request

请求上下文(request context)

思考:在视图函数中,如何取到当前请求的相关数据?比如:请求地址,请求方式,cookie等等

在 flask 中,可以直接在视图函数中使用 request 这个对象进行获取相关数据,而 request 就是请求上下文提供的对象,保存了当前本次请求的相关数据,请求上下文提供的对象有:request、session

所以每次客户端不同的请求,得到的request和session的对象都是同一个,但是内部的数据都是不一样的。

  • request

    • 封装了HTTP请求的内容,针对的是http请求。举例:user = request.args.get('user'),获取的是get请求的参数。

  • session

    • 用来记录请求会话中的信息,针对的是用户信息。举例:session['name'] = user.id,可以记录用户的状态信息。还可以通过session.get('name')获取用户的状态信息。

请求上下文提供的变量/属性/方法/函数/类与对象,只能在视图中或者被视图调用的地方使用

  代码:

 1 from flask import Flask, request, session
 2 
 3 
 4 app = Flask(__name__)
 5 
 6 app.config.update({
 7     "DEBUG": True,
 8     "SECRET_KEY": "sklaasle3k2334"
 9 })
10 
11 
12 @app.route("/")
13 def index():
14     print(request, id(request), request.args)
15     return "ok!"
16 
17 
18 if __name__ == '__main__':
19     # request写在这里,就表示超出了用户请求和响应流程之外的地方了.会报错.
20     # print(request)  # RuntimeError: Working outside of request context. requset不能在情趣上传文以外的地址被调用
21     # print(session)
22     app.run()

应用上下文(application context)

它的字面意思是 应用上下文,但它不是一直存在的,它只是request context 中操作当前falsk应用对象 app 的代理对象,就是所谓本地代理(local proxy)。它的作用主要是帮助 request 获取当前的flask应用相关的信息,它是伴 request 而生,随 request 而灭的。

应用上下文提供的对象有:current_app,g

current_app

应用程序上下文,用于存储flask应用实例对象中的变量,可以通过current_app.name打印当前app的名称,也可以在current_app中存储一些变量,例如:

  • 应用的启动脚本是哪个文件,启动时指定了哪些参数

  • 加载了哪些配置文件,导入了哪些配置

  • 连接了哪个数据库

  • 有哪些可以调用的工具类、常量

  • 当前flask应用在哪个机器上,哪个IP上运行,内存多大

代码:

 1 from flask import Flask,request,session,current_app,g
 2 
 3 # 初始化
 4 app = Flask(import_name=__name__)
 5 
 6 # 声明和加载配置
 7 class Config():
 8     DEBUG = True
 9 app.config.from_object(Config)
10 
11 # 编写路由视图
12 @app.route(rule='/')
13 def index():
14     # 应用上下文提供给我们使用的变量,也是只能在视图或者被视图调用的地方进行使用,
15     # 但是应用上下文的所有数据来源于于app,每个视图中的应用上下文基本一样
16     print(current_app.config)   # 获取当前项目的所有配置信息
17     print(current_app.url_map)  # 获取当前项目的所有路由信息
18 
19     return "<h1>hello world!</h1>"
20 
21 if __name__ == '__main__':
22     # 运行flask
23     app.run(host="0.0.0.0")

g变量

g 作为 flask 程序全局的一个临时变量,充当者中间媒介的作用,我们可以通过它传递一些数据,g 保存的是当前请求的全局变量,不同的请求会有不同的全局变量,通过不同的thread id区别

g.name='abc' # name是举例,实际要保存什么数据到g变量中,可以根据业务而定,你可以任意的数据进去

代码:

 1 from flask import Flask,request,session,current_app,g
 2 
 3 # 初始化
 4 app = Flask(import_name=__name__)
 5 
 6 # 声明和加载配置
 7 class Config():
 8     DEBUG = True
 9 app.config.from_object(Config)
10 
11 @app.before_request
12 def before_request():
13     g.name = "root"
14 
15 # 编写路由视图
16 @app.route(rule='/')
17 def index():
18     # 请求上下文提供的变量/属性/方法/函数/类与对象,只能在视图中或者被视图调用的地方使用
19     # 请求上下文里面信息来源于每次客户端的请求,所以每个视图中请求上下文的信息都不一样
20     # print(session)
21 
22     # 应用上下文提供给我们使用的变量,也是只能在视图或者被视图调用的地方进行使用,
23     # 但是应用上下文的所有数据来源于于app,每个视图中的应用上下文基本一样
24     print(current_app.config)   # 获取当前项目的所有配置信息
25     print(current_app.url_map)  # 获取当前项目的所有路由信息
26     index2()
27     index3()
28     return "ok!"
29 
30 
31 def index2():
32     print(g.name)
33 
34 def index3():
35     print(g.name)
36 
37 
38 if __name__ == '__main__':
39     # 默认情况下,应用上下文提供的对象,也只能在客户端的请求与响应阶段进行调用。
40     # 但是工作中往往,需要在请求响应之外,调用服务端信息,此时,就必须要在请求响应以外的地方调用current_app
41     # 例如:回头使用celery实现异步任务或是定时任务,那么如果任务里面需要操作数据,则必须调用项目配置,那么就一定要使用current_app
42     with app2.app_context():
43         print(current_app)
44         print(g)
45     app2.run()

两者的区别:

  • 请求上下文:保存了客户端和服务器交互的数据,一般来自于客户端。

  • 应用上下文:flask 应用程序运行过程中,保存的一些配置信息,比如路由列表,程序名、数据库连接、应用信息等

    应用上下文提供的对象,可以直接在请求上下文中使用,但是如果在请求上下文之外调用,则需要使用

    with app.app_context()创建一个应用上下文环境才能调用。

四、终端脚本命令

在flask1.1版本之前版本中都是采用flask-script模块来执行终端脚本命令,flask1.1版本以后不再使用这个模块了,因为存在兼容性问题。

flask0.11.0版本以后,flask内置了一个Click模块,这个模块是终端命令模块,可以让我们直接通过Click的装饰器,编写和运行一些终端命令。在flask2.0版本已经不能兼容flask-script模块了,所以需要改成使用Click模块来运行和自定义管理终端命令了。

安装了flask2.0以后,当前项目所在的python环境就提供了一个全局的flask命令,这个flask命令是Click提供的。

# 要使用Click提供的终端命令flask,必须先在环境变量中声明当前flask项目的实例对象所在的程序启动文件。
# 例如:manage.py中使用了 app = Flask(__name__),则manage.py就是程序启动文件


# 使用flask终端命令之前,可以配置2个环境变量。
# 指定入口文件,开发中入口文件名一般:app.py/run.py/main.py/index.py/manage.py/start.py
export FLASK_APP=manage.py
# 指定项目所在环境
export FLASK_ENV=development   # 开发环境,默认开启DEBUG模式
# export FLASK_ENV=production   # 生成环境,默认关闭DEBUG模式

默认情况下,flask命令提供的子命令。

flask routes  # 显示当前项目中所有路由信息
flask run     # 把flask项目运行在内置的测试服务器下
# flask run --host=0.0.0.0 --port=5055
flask shell   # 基于项目的应用上下文提供终端交互界面,可以进行代码测试。

Click自定义终端命令

import click
from flask import Flask, views

app = Flask(__name__)
# 配置
app.config.update({
    "DEBUG": False,
})


# 自定义终端命令
@app.cli.command("faker")                                                # 假设这个用于生成测试数据
@click.argument("data", default="user")                                  # data表示生成数据的类型[参数argument是命令调用时的必填参数]
@click.option('-n', 'number', type=int, default=1, help='生成的数据量.')   # num表示测试数据的生成数量[选项option是命令调用时的可选参数]
def faker_command(data, number):
    """添加测试信息"""
    print("添加测试信息")
    print(f"数据类型:data={data}")
    print(f"生成数量:number={number}")


@app.route("/")
def index():
    return "ok"


if __name__ == '__main__':
    app.run()
    
"""
flask faker --help
flask faker -n10 user
flask faker user
"""

终端下的运行效果:

(flask) moluo@ubuntu:~/Desktop/flaskdemo$ flask faker -n10 user
添加测试信息
数据类型:data=user
生成数量:number=10
(flask) moluo@ubuntu:~/Desktop/flaskdemo$ flask faker user
添加测试信息
数据类型:data=user
生成数量:number=1
(flask) moluo@ubuntu:~/Desktop/flaskdemo$ flask faker goods
添加测试信息
数据类型:data=goods
生成数量:number=1

练习:

1. flask2.0的终端下,输入 python manage.py startapp home 则可以在当前目录下创建以下目录和文件
项目目录/
 └── home
     ├── views.py
     ├── models.py
     ├── urls.py
     └── tests.py

 代码:

 

import click, os
from flask import Flask


app = Flask(__name__)
# 配置
app.config.update({
    "DEBUG": False
})


@app.cli.command("startapp")
@click.argument("name")
# @click.option('-n', 'name', help='app name')
def startapp(name):
    """生成子模块或子应用"""
    if os.path.isdir(name):
        print(f"当前{name}目录已存在!请先处理完成以后再创建。")
        return

    os.mkdir(name)
    open(f"{name}/views.py", "w")
    open(f"{name}/models.py", "w")
    open(f"{name}/documents.py", "w")
    open(f"{name}/ws.py", "w")
    open(f"{name}/services.py", "w")
    open(f"{name}/urls.py", "w")
    open(f"{name}/test.py", "w")
    print(f"{name}子应用创建完成....")


@app.route("/")
def index():
    return "ok"


if __name__ == '__main__':
    app.run()

终端调用:

flask startapp home
flask startapp users

  六、jinja2模板引擎

Flask内置的模板语言Jinja2,它的设计思想来源于 Django 的模板引擎DTP(DjangoTemplates),并扩展了其语法和一系列强大的功能。

  • Flask提供的 render_template 函数封装了该模板引擎Jinja2

  • render_template 函数的第一个参数是模板的文件名,后面的参数都是键值对,表示模板中变量对应的数据值。

模板基本使用

1、在flask应用对象创建的时候,设置或者保留template_folder参数,创建模板目录

app = Flask(__name__,template_folder='templates')

2、在项目下手动创建 templates 文件夹,用于存放所有的模板文件,并在目录下创建一个模板html文件 index.html

 

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{ title }}</title>
</head>
<body>
    <h1>{{ content }}</h1>
</body>
</html>

3、在视图函数设置渲染模板并设置模板数据

from flask import Flask, render_template

# 默认情况下,flask默认直接支持视图加载模板的。
# Flask类在实例化的时候,默认当前根目录下的templates目录作为模板目录
app = Flask(__name__, template_folder="templates")
# 配置
app.config.update({
    "DEBUG": True
})

@app.route("/")
def index():
    data = {
        "title": "我的模板标题",
        "content": "我的模板内容"
    }
    return render_template("index.html", **data)

@app.route("/user/<id>")
def user(id):
    title = "User Center"
    content = "我的个人中心"
    print(locals()) # {'id': '100', 'title': 'User Center', 'content': '我的个人中心'}
    return render_template("user.html", **locals())

if __name__ == '__main__':
    app.run()