flask-cbv源码分析、模板、请求与响应、session、闪现、请求扩展
1.flask写cbv
1.1 cbv模板
之前我们都是写fbv,现在我们写cbv,这样可以把get请求写在一个视图类中:
from flask import Flask, request
from flask.views import View, MethodView
app = Flask(__name__)
'''每次修改之后项目会自己启动'''
app.debug = True
class IndexView(MethodView):
'''在这里不用再把request加入形参,直接使用全局的request就可以'''
def get(self):
print(request.method)
return 'get请求'
def post(self):
print(request.method)
return 'post 请求'
'''endpoint是别名,as_view()括号内也要写别名,后面详解'''
app.add_url_rule('/index',endpoint='index',view_func=IndexView.as_view('index'))
if __name__ == '__main__':
app.run()
1.2 cbv源码分析
1.首先我们需要从as_view()源码中分析,点进原码:
def as_view(
cls, name: str, *class_args: t.Any, **class_kwargs: t.Any
) -> ft.RouteCallable:
def view(**kwargs: t.Any) -> ft.ResponseReturnValue:
self = view.view_class( # type: ignore[attr-defined]
*class_args, **class_kwargs
)
# 不管走if分支还是else分支,都会返回一个current_app.ensure_sync(self.dispatch_request)(**kwargs)。这句代码其实是在执行self.dispatch_request,只不过是异步执行
return current_app.ensure_sync(self.dispatch_request)(**kwargs)
return view
2.当请求来了之后,执行view(),本质还是在执行self.dispatch_request,所以我们现在需要找到dispatch_request源码(我们直接点进去发现什么都没有,这是因为我们直接点进去的是View下的dispatch_request源码,而我们的self是视图类的对象,查找顺序是:对象名称空间(没有)>>>视图类名称空间(没有)>>>MethodView名称空间(找到了)):
def dispatch_request(self, **kwargs: t.Any) -> ft.ResponseReturnValue:
meth = getattr(self, request.method.lower(), None)
'''meth通过反射拿到了视图类中的post或者get,此时的meth就是get或者post'''
if meth is None and request.method == "HEAD":
meth = getattr(self, "get", None)
'''异步执行了get()或者post()'''
return current_app.ensure_sync(meth)(**kwargs)
1.3 as_view()参数分析
当我们在as_view()中不传参数时,项目无法启动。所以as_view()中必须要传参数。这是因为as_view()的返回值是view,所以如果我们不传参数,所有的返回值都是view,结果一样那么必然会报错。
view源代码:
'''view中的形参**kwargs使用了其外层函数as_view中的参数,所以这是一个闭包函数'''
def view(**kwargs: t.Any) -> ft.ResponseReturnValue:
self = view.view_class( # type: ignore[attr-defined]
*class_args, **class_kwargs
)
return current_app.ensure_sync(self.dispatch_request)(**kwargs)
...
view.view_class = cls # type: ignore
view.__name__ = name
view中的形参使用了as_view()中的参数,所以这是一个闭包函数。view.name = name,view就是闭包函数内的view,这句代码是修改了函数的名字。
拓展:当我们print(函数名.name)拿到的是一个函数名,当然我们也可以自己修改其名称,再次打印其名字就是修改之后的:
def index():
pass
print(index.__name__) # index
index.__name__ = 'add'
print(index.__name__) # add
紧接上面,其实我们可以简写生成路由代码:
app.add_url_rule('/index',view_func=IndexView.as_view('index'))
# 等同于:
app.add_url_rule('/index',view_func=view)
所以如果我们不传endpoint参数,并且不在as_view()中传参数的话,那么所有的view都是一个,就会报错。
下面我们来看endpoint和as_view()中参数的关系,首先查看add_url_rule源码:
@setupmethod
def add_url_rule(
self,
rule: str,
endpoint: t.Optional[str] = None,
view_func: t.Optional[ft.RouteCallable] = None,
provide_automatic_options: t.Optional[bool] = None,
**options: t.Any,
) -> None:
if endpoint is None:
endpoint = _endpoint_from_view_func(view_func) # type: ignore
查看_endpoint_from_view_func源码:如果endpoint没有传入,执行以下代码,直接返回view_func对应函数的名字。所以如果路径不传endpoint,那么别名就是函数名:
def _endpoint_from_view_func(view_func: t.Callable) -> str:
assert view_func is not None, "expected view func if endpoint is not provided."
return view_func.__name__
以后我们只要在as_view()中填写别名,就不需要再传入endpoint参数了
1.4 视图类加装饰器
如果我们要给某个视图类加装饰器,以登录认证为例,我们需要将装饰的函数写在类中decorators的列表中:
class IndexView(MethodView):
decorators = [loginauth]
def get(self):
print(request.method)
return 'get请求'
def post(self):
print(request.method)
return 'post 请求'
阅读decorators源码得知:
if cls.decorators:
for decorator in cls.decorators:
view = decorator(view)
decorator就是一个个认证的函数,在这里就是loginauth,view=loginauth(view),这就是装饰器的原理,这句代码相当于:
@loginauth
def view():
pass
1.5 补充
如果我们给flask的视图函数加一个登录认证装饰器,那么应该放在路由装饰器的下面,并且route中需要传入endpoint参数。
总结:
1.as_view执行流程和django一样
2.如果写视图类,那么必须继承MethodView(view中的dispatch_request直接抛异常,所以继承的父类必须要重写dispatch_request),否则需要重写dispatch_request
3.视图函数加多个装饰器,必须要传入endpoint
4.路径如果不传endpoint,那么别名就是函数名
5.视图类加装饰器,按照:decorators = [...]传入
2.模板语法
首先我们生成app对象时,需要上传一些参数,点击Flask查看参数:
def __init__(
self,
import_name: str,
static_url_path: t.Optional[str] = None,
static_folder: t.Optional[t.Union[str, os.PathLike]] = "static",
'''静态文件,所以我们创建静态文件时路径和文件名需要从这里传入,这里只是一个默认值,如果不传参数就按照默认值'''
static_host: t.Optional[str] = None,
host_matching: bool = False,
subdomain_matching: bool = False,
template_folder: t.Optional[t.Union[str, os.PathLike]] = "templates",
'''之所以我们要在根目录创建templates目录,是因为在这里有规定,如果我们的路径和文件名有变化,则需要从这里传值修改'''
instance_path: t.Optional[str] = None,
instance_relative_config: bool = False,
root_path: t.Optional[str] = None,
):
模板语法可以在前端显示图片,实现if判断,for循环,放置链接 ,执行函数:
flask.py
from flask import Flask, render_template, Markup
app = Flask(__name__, template_folder='templates', static_folder='static')
def add(a, b):
return a + b
@app.route('/')
def index():
# 方式一:后端直接把标签引起来,前端使用{{a|safe}}
a = '<a href="https://www.cnblogs.com/ERROR404Notfound/p/16983752.html">点我看美女</a>'
# 方式二:后端使用Markup包起来,前端使用{{a}}
a = Markup(a)
return render_template('index.html', name='max', a=a,add=add)
if __name__ == '__main__':
app.run()
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>模板语法,照片渲染</h1>
<img src="/static/1.png" alt="">
<h1>模板语法,if</h1>
{% if name %}
<p>{{name}}</p>
{% else %}
<p>anonymous</p>
{% endif %}
<h1>链接</h1>
{{a}}
<h1>执行函数</h1>
{{add(2,3)}}
</body>
</html>
执行结果:
3.请求与响应
3.1 request的方法
首先发送get请求,请求路由:http://127.0.0.1:5000/?name=max
from flask import Flask, request
app = Flask(__name__)
# app.debug = True
@app.route('/', methods=['GET', 'POST'])
def index():
print('request.method',request.method) # GET
print('request.args',request.args) # ImmutableMultiDict([('name', 'max')])
print('request.form',request.form) # ImmutableMultiDict([])
print('request.values',request.values) # CombinedMultiDict([ImmutableMultiDict([('name', 'max')])])
print('request.cookies',request.cookies) # ImmutableMultiDict([])
print('request.headers',request.headers)
'''
Host: 127.0.0.1:5000
Connection: keep-alive
Sec-Ch-Ua: "Microsoft Edge";v="111", "Not(A:Brand";v="8", "Chromium";v="111"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.62
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
''' print('request.path',request.path) # /
print('request.full_path',request.full_path) # /?name=max
print('request.url',request.url) # http://127.0.0.1:5000/?name=max
print('request.base_url',request.base_url) # http://127.0.0.1:5000/
print('request.host_url',request.host_url) # http://127.0.0.1:5000/
print('request.host',request.host) # 127.0.0.1:5000
return '111'
if __name__ == '__main__':
app.run()
接下来用postman发送post请求,请求地址为:
http://127.0.0.1:5000?name=max&age=22, 在body体中携带了键值对{"hobby":"soccer","file":文件对象}
from flask import Flask, request
app = Flask(__name__)
# app.debug = True
@app.route('/', methods=['GET', 'POST'])
def index():
print('request.method',request.method) # POST
print('request.args',request.args) # ImmutableMultiDict([('name', 'max'), ('age', '22')])
print('request.form',request.form) # ImmutableMultiDict([('hobby', 'soccer')])
print('request.values',request.values) # CombinedMultiDict([ImmutableMultiDict([('name', 'max'), ('age', '22')]), ImmutableMultiDict([('hobby', 'soccer')])])
print('request.cookies',request.cookies) # ImmutableMultiDict([])
print('request.headers',request.headers)
'''
User-Agent: PostmanRuntime/7.31.3
Accept: */*
Postman-Token: 7e19ce43-ead9-47fa-b364-6e49032b735c
Host: 127.0.0.1:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: multipart/form-data; boundary=--------------------------721949126982998088027215
Content-Length: 42306
'''
print('request.path',request.path) # /
print('request.full_path',request.full_path) # /?name=max&age=22
print('request.url',request.url) # http://127.0.0.1:5000/?name=max&age=22
print('request.base_url',request.base_url) # http://127.0.0.1:5000/
print('request.host_url',request.host_url) # http://127.0.0.1:5000/
print('request.host',request.host) # 127.0.0.1:5000
'''文件操作'''
obj = request.files['file']
print(obj) # <FileStorage: '01cea05acc4cc0a801204029f30fa6.jpg@2o.png' ('image/png')>
obj.save(obj.filename)
return '111'
if __name__ == '__main__':
app.run()
3.2 响应写入cookies
from flask import Flask, request,make_response
app = Flask(__name__)
# app.debug = True
@app.route('/', methods=['GET', 'POST'])
def index():
response = 'hello world'
res = make_response(response)
'''这时的res是一个对象,但是依然可以在前端正常显示'''
print(type(res)) # <class 'flask.wrappers.Response'>
res.set_cookie('aa','bb')
return res
if __name__ == '__main__':
app.run()
3.3 响应头中写数据
from flask import Flask, request,make_response
app = Flask(__name__)
# app.debug = True
@app.route('/', methods=['GET', 'POST'])
def index():
response = 'hello world'
res = make_response(response)
print(type(res))
res.headers['cc'] = 'dd'
return res
if __name__ == '__main__':
app.run()
跨域在没有第三方模块的情况下也可以用这种方式来解决
4.session使用及其源码分析
4.1 session的使用
flask.py
from flask import Flask,request,render_template,session,redirect
app = Flask(__name__)
app.debug = True
app.secret_key = 'fiuhgy487y97tufgiorewy'
@app.route('/login',methods=['POST','GET'])
def login():
if request.method == 'GET':
return render_template('login.html')
else:
name = request.form.get('name')
password = request.form.get('password')
if name and password:
'''如果用户名和密码有值,就重定向到/index'''
session['name'] = name
return redirect('/index')
@app.route('/index',methods=['GET','POST'])
def index():
return 'hello %s' % session.get('name','匿名用户')
if __name__ == '__main__':
app.run()
login.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form method="post">
<p>用户名:<input type="text" name="name"></p>
<p>密码:<input type="password" name="password"></p>
<p><input type="submit" value="提交"></p>
</form>
</body>
</html>
虽然我们使用的都是全局的session,但是在不同浏览器中取到的值却不一样,flask可以很好地管理不同浏览器之间的信息。但是只要用同一个浏览器同时登陆两次,那么session中第一次保存的session就会被取代掉。
4.2 session源码分析
cookies:存在于客户端浏览器中的键值对
session:存在于服务端的键值对(django存在了django_session表中)
当我们在django中设置一个已经有的session键对应的值,django_session表中的键还是不变,变的是该键对应的值。但是在flask中没有session表。而flask将该信息存储到了浏览器中的cookies中,它的名字叫session但是它并不是一个真正的session,也不是一个cookie,因为cookie是一个键值对,而这里只有一个信息:
当我们再换一个账号登陆,发现该session会变化。当我们删掉该session,发现再次刷新/index页面,发现名字变成了匿名用户:
那么当我们在后端执行了代码:session['name'] = name后后端做了什么事呢?
1.首先是上述代码校验是否有用户名和密码,如果有,执行代码session['name'] = name
2.将session对象中的key-value组成一个字符串
3.将该字符串返回到前端,返回到浏览器中存储
4.浏览器中存储了session后,浏览器发送请求时会携带该cookie,后端取出携带过来的字符串,转到session中
5.咱们在视图函数中使用session(真正的session)中取到上次放进去的值
而在django中,后端随机生成一个字符串,将它保存在session表中,并且返回给前端一份。此时前端在发请求就会携带该随机字符串(cookie)到后端,后端取出cookie,并且区session表中做校验(在中间件中)。
4.2 源码分析
app.session_interface中配置了一个类的对象,这个就是session的执行流程,点进查看:
session_interface: SessionInterface = SecureCookieSessionInterface()
查看类SecureCookieSessionInterface源码:
class SecureCookieSessionInterface(SessionInterface):
...
'''请求来的时候会走open_session'''
def open_session(
self, app: "Flask", request: "Request"
) -> t.Optional[SecureCookieSession]:
s = self.get_signing_serializer(app)
if s is None:
return None
'''根据名字,取出前端传入的cookie值'''
val = request.cookies.get(self.get_cookie_name(app))
'''如果没有值,构造了一个空的session对象'''
if not val:
return self.session_class()
max_age = int(app.permanent_session_lifetime.total_seconds())
try:
'''如果没有过期,把字符串解密,做成session对象,后续直接用session对象'''
data = s.loads(val, max_age=max_age)
return self.session_class(data)
except BadSignature:
'''如果过期提供的cookie也就不能用了,返回一个空session对象'''
return self.session_class()
在上述代码中有一个get_cookie_name方法,这行代码就是在配置文件中拿到浏览器中cookie的键名,如果这个配置修改,那么前端cookie中的键名也会修改:
def get_cookie_name(self, app: "Flask") -> str:
return app.config["SESSION_COOKIE_NAME"]
当请求走的时候会执行
def save_session(
self, app: "Flask", session: SessionMixin, response: "Response"
) -> None:
expires = self.get_expiration_time(app, session)
'''把session加密转成字符串,存到cookie中'''
val = self.get_signing_serializer(app).dumps(dict(session)) # type: ignore
response.set_cookie(
name,
val, # type: ignore
expires=expires,
httponly=httponly,
domain=domain,
path=path,
secure=secure,
samesite=samesite,
)
5.闪现
当次请求先把一些数据,放在某个位置,下一次请求把这些数据取出来,一次取出就没了。
作用:
1.可以跨请求保存数据
2.当次请求,访问出错,被重定向到其他地址
django中也有该内容,就是message框架,django中有级别,flask中没有级别
from flask import Flask, request, render_template, session, redirect, flash, get_flashed_messages
app = Flask(__name__)
app.secret_key='fghyeu56t378r92wyfgh'
@app.route('/set')
def set_flash():
name = request.args.get('name')
flash('我是%s' % name)
return 'flash写入'
@app.route('/get')
def get_flash():
res = get_flashed_messages()
print(res)
return 'flash取出'
if __name__ == '__main__':
app.run()
我们首先执行取出方法:
没有存入所以取出来的为空:
然后再执行写入方法,再取出,发现已经有值:
再取出一次取出的值为空,所以只能取一次
并且在另一个浏览器中,存入的信息不一样,取出的信息也不同
6.请求扩展
请求扩展中,请求来了,或者请求走了,可以绑定一些函数,到这里就会执行这个函数,类似于django的中间件
请求扩展有以下几种:
before_request:请求来了会走,如果他返回了四件套,就结束了
after_request :请求走了会走,一定要返回response对象
before_first_request:第一次来了会走
teardown_request:无论是否出异常,会走
errorhandler:监听状态码,404 500
template_global:标签
template_filter:过滤器
1.before_request和after_request:
from flask import Flask
app = Flask(__name__)
@app.before_request
def before():
print('请求来了')
@app.after_request
def after(res):
print('请求走了')
return res
@app.route('/index')
def index():
return 'index'
if __name__ == '__main__':
app.run()
如果有多个,before_request会从上往下依次执行,after_request会从下往上依次执行:
from flask import Flask
app = Flask(__name__)
@app.before_request
def before():
print('请求来了1')
@app.before_request
def before1():
print('请求来了2')
@app.after_request
def after(res):
print('请求走了1')
return res
@app.after_request
def after(res):
print('请求走了2')
return res
@app.route('/index')
def index():
return 'index'
if __name__ == '__main__':
app.run()
请求走的时候我们设置cookie:
@app.after_request
def after(res):
print('请求走了1')
res.set_cookie('name','max')
return res
在before_request中返回新手四件套之一,就会拦截请求:
@app.before_request
def before():
print('请求来了1')
return '不让走了'
2.before_first_request:第一次请求会执行,此后不会再执行
from flask import Flask
app = Flask(__name__)
@app.before_first_request
def first():
print('第一次执行才会显示')
@app.route('/index')
def index():
return 'index'
if __name__ == '__main__':
app.run()
3.teardown_request:每一次请求来时候都会执行,有一个形参,是报错时才会触发,该形参就是错误,没有错误时依旧会执行该函数体代码,只不过形参是None,用来做错误日志:
from flask import Flask
app = Flask(__name__)
@app.teardown_request
def tear_down(e):
print('teardown执行了')
print(e)
@app.route('/index')
def index():
l = [1,2]
print(l[4])
return 'index'
if __name__ == '__main__':
app.run()
4.errorhandler 监听响应状态码,如果符合监听的状态码,就会走它
from flask import Flask
app = Flask(__name__)
'''也可以有@app.errorhandler(500)'''
@app.errorhandler(404)
def error_404(a):
return '页面不存在'
@app.route('/index')
def index():
l = [1,2]
print(l[4])
return 'index'
if __name__ == '__main__':
app.run()
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)