Flask入门
Flask入门
常见python-web框架:
django:大而全的web框架,自己内置了很多app,第三方适配的模块也多,但由于过于全,在开启小项目时,略显臃肿。
flask:小而精的python-web框架,甚至可以在一个py文件中完成web最基础的功能,而完成更丰富的功能则需要借助第三方模块。
web.py:是一个小巧灵活的Python框架,它简单而且功能强大(国内几乎没有用的)
常见的异步web框架:
fastapi:python的异步web框架,不少公司在用,专门做前后端分离用于写接口
https://fastapi.tiangolo.com/zh/
sanic:python的异步web框架,供支持异步高并发请求的 web 服务
tornado:异步框架,用的比较少了:
这里需要强调所谓同步框架和异步框架,是有本质上的区别的:
同步框架并不代表没有使用异步,而是对于处理请求到返回响应的过程是同步的,一个线程只能处理一个请求,请求在响应前一直占据此线程。同步框架项目也会有异步并发,如django就可以同时处理多个请求(虽然并发量很小)是通过网关服务器实现的。
异步框架则可以灵活的调度线程,当处理请求的过程中出现了IO等阻塞操作,就会将当前线程重新分配用于处理其他请求,可以非常显著的提高cpu的利用率和并发量。
简单来说:
- 同步框架的一个线程只能处理一个请求
- 异步框架的一个线程可以处理多个请求
flask简介
Flask是一个基于Python开发并且依赖jinja2模板和Werkzeug WSGI服务的一个微型框架。
jinja2是支持模板语法的模块,可以用于在html文档中用特殊的模板语法进行插值渲染,主要用于前后端混合的项目。
Werkzeug WSGI 符合wsgi协议的web服务器,与django使用的wsgiref是不同的web服务器。
Hello World
from flask import Flask
app = Flask(__name__) # 初始化app
@app.route('/') # 注册路由
def index(): # 视图函数
return 'hello world'
if __name__ == '__main__':
app.run() # 启动项目
前后端混合项目演示
这个小项目用了古老的表单标签(使用form标签可以发送post请求的特性)。
html模板文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form method="post">
<input type="text" name="num1"> +
<input type="text" name="num2"> = {{summary}}
<input type="submit" value="计算"> {{error}}
</form>
</body>
</html>
py文件(项目后端)
from flask import Flask, request, render_template
app = Flask(__name__)
# 根路径
@app.route('/', methods=['GET', 'POST'])
def index():
# flask不需要传入request对象,使用全局的request
if request.method == 'GET':
return render_template('login.html') # 返回模板
else:
if num1.isdigit() and num2.isdigt():
return render_template('sum.html',summary=int(num1)+int(num2))
else:
return render_template('login.html', error='输入的不是纯数字') # 注意跟django的render区分,要模板渲染的数据,直接key=value传即可
首先浏览器按照路由发送get请求拿到html文件,在页面中,输入两个数字,点击提交,则会向本地址(相同路由)发送post请求,再进入post的分支,携带插值变量渲染html模板返回前端。
flask基础
-
注册路由可以通过装饰器
app.route(路径,methods=[请求方式,'get','post'])
,加装在某个函数上,这个函数就是视图函数了,路径中可以使用转换器动态匹配路由 -
视图函数必须返回响应内容,基础的有:
- render_template 按模板返回
- redirect 重定向到其他路由
- 字符串 与django的HttpResponse类似
- jsonify 返回json格式字符串(内部可以填入列表,字典等)
-
请求的request对象,是全局的,直接导入使用即可,在不同视图函数中不会混乱
-
session 全局的,直接导入使用即可
要使用session前一定要指定秘钥app.secret_key = '越复杂越好'
-
模板渲染使用的jinjia2,比django的模板语法更强大,可以加括号传参调用,还可以使用中括号取值等。
flask配置方式
flask有多种配置方式,在此罗列一下:
-
flask对象
app=Flask()
中,可以app.debug
和app.secret_key
,配置debug和秘钥app.config['配置键'] = 配置值
可以设置其他配置项 -
使用py文件导入(类似于django的settings.py)
app.config.from_pyfile("settings.py")
类似于django项目中的策略,但在flask中还有更好的方案。
-
(常用)导入类配置项
## settings.py class ConfigBase: DEBUG=False 一些配置项。。。 class DevConfig(ConfigBase): DEBUG=True # 开发环境中debug为true mysql=测试库 # 开发时用测试库 class ProdConfig(ConfigBase): mysql=上线库 # 实际上线用不一样的库配置 ## 导入配置 app.config.from_object('settings.DevConfig')
-
其他:
# 通过环境变量配置 app.config.from_envvar("环境变量名称") # 通过json文件配置 app.config.from_json("json文件名称") # 通过字典格式配置 app.config.from_mapping({'DEBUG': True})
字典格式配置在一些大型公司中会用到,因为会建立大的配置中心,多台机器可能都使用这个配置中心的配置,启动时先朝配置中心发送一个请求,它会返回一个字典(验证通过时),方便我们做集群化部署等。
对于项目的配置,我们可以定义为,供给给项目启动后自动加载的不再变化的量。而这些配置可以是flask的内置配置字段,用于支持flask的web服务,也可以是其他模块的配置字段,如redis、mysql的连接地址等。
路由系统
写法及原理
flask配置路由的基础写法是装饰器:
@app.route('/')
def index():
pass
也就是将index这个普通函数替换成了带路由的视图函数。按照有参装饰器的语法糖,index被替换为app.route('/')(index),查看route的源码,即被替换成了decorator(index)这个闭函数。
其内部最核心的就只执行了self.add_url_rule(rule, endpoint, f, **options)
,而self在这个语境中是flask的对象app。
所以flask路由的本质是app对象的add_url_rule完成路由的注册
add_url_rule的参数
rule 匹配的规则(路径,可能含转换器)
view_func 视图函数,即被装饰的函数
defaults = None 视图函数可能会需要一些额外的参数,通过defaults = {'k': 'v'}提供
endpoint = None 路径的别名,名称,用于反向解析URL,反向解析用到一个函数url_for('endpoint名称')
methods = None 允许的请求方式,如:["GET", "POST"]
strict_slashes = None 对URL最后的 / 符号是否严格要求
redirect_to = None 访问这个路由相当于重定向到redirect_to
转换器
路由中可能包含一些视图函数所需的参数,通过转换器对路由的某一部分进行动态匹配并传参给视图。
使用方式:
@app.route('/student/<int:pk>')
@app.route('/media/<path:path>')
转换器的格式为<转换器类型:匹配内容的变量名>
转换器类型有:
转换器 | 说明 |
---|---|
default | UnicodeConverter |
string | UnicodeConverter |
any | AnyConverter |
path | PathConverter |
int | IntegerConverter |
float | FloatConverter |
uuid | UUIDConverter |
flask的CBV
# 基于类的视图,写法
from flask import Flask,request
from flask.views import View, MethodView # View是MethodView的基类
app = Flask(__name__)
app.debug = True
# 视图类,继承MethodView,类中写跟请求方式同名的方法即可
class IndexView(MethodView):
def get(self):
print(request.method)
return 'get 请求'
def post(self):
print(request.method)
return 'post 请求'
app.add_url_rule('/index', endpoint='index', view_func=IndexView.as_view('index'))
if __name__ == '__main__':
app.run()
与视图函数比较,只是app.add_url_rule添加路由时形参view_func传入的内容为类.as_view('别名')
,视图类由于继承了MethodView,所以是执行了其内部的as_view方法,最终其实返回的还是一个函数的内存地址。
源码解析
as_view
def as_view(cls, name, *class_args, **class_kwargs):
def view(**kwargs: t.Any) -> ft.ResponseReturnValue:
# 本质是在执行self.dispatch_request,只是用了异步
return current_app.ensure_sync(self.dispatch_request)(**kwargs)
return view
dispatch_request
def dispatch_request(self, **kwargs):
# self是视图类的对象,这里反射拿到请求方式对应的视图函数
meth = getattr(self, request.method.lower(), None) # 如拿到self.get
# 用异步执行meth()
return current_app.ensure_sync(meth)(**kwargs)
add_url_rule的endpoint=None
-@app.route('/index')--》没有传endpoint
-endpoint 就是None---》调用了app.add_url_rule,传入了None
# 路径如果不传别名,别名就是函数名
if endpoint None:
endpoint = _endpoint_from_view_func(view_func) # type: ignore
-_endpoint_from_view_func -->return view_func.__name__
为什么要设置endpoint参数?
因为有时视图函数会先被其他装饰器装饰后再装饰路由,那么传入view_func的函数实际上都是其他装饰器的名字,很可能造成冲突,需要手动传入一个视图名。
as_view必传别名
as_view是一个包函数,其内部是一个闭函数view,add_url_rule会将endpoint都处理成view,那么就报错了,但是如果要求传入name,那么as_view内部会将view的name先替换掉view.__name__ = name
,那么不同的视图类就不会冲突了。
MethodView继承View
View中实现了as_view方法,其中执行了self.dispatch_request,这个方法没有在View中实现,所以View是抽象类,它需要子类去实现这个方法,MethodView就实现了dispatch_request方法。
视图类加装饰器
实际上是给所有的类中的函数加装饰器。
class IndexView(MethodView):
decorators = [auth,]
def get(self):
pass
return 'get 请求'
def post(self):
pass
return 'post 请求'
# as_view中从左到右装饰原函数
if cls.decorators:
view.__name__ = name
view.__module__ = cls.__module__
for decorator in cls.decorators:
view = decorator(view)
jinjia2模板语法
与django的dtl语法相比,多了以下几条特性:
使用模板
flask项目中默认配置了模板的路径为templates,也可以通过参数template_folder更改
app = Flask(__name__, template_folder='templates', static_folder='static')
函数可加括号传参
{{ add(4,5) }}
Markup(字符)取消校验
一般文本插值到html中,会被处理变成非html的文本,使用markup后直接按标签渲染。
自定义标签和过滤器
# template_global 在模板中直接使用该过滤器
@app.template_global()
def add(a1, a2):
return a1 + a2
# template_filter
@app.template_filter()
def db(a1, a2, a3):
return a1 + a2 + a3
ps:jinjia2这种前后端混合语法已经逐渐不再使用
请求与响应
请求属性
request的属性总结 |
---|
request.method 提交的方法 |
request.args get请求提及的数据 |
request.form post请求提交的数据 |
request.values post和get提交的数据总和 |
request.cookies 客户端所带的cookie |
request.headers 请求头 |
request.path 不带域名,请求路径 |
request.full_path 不带域名,带参数的请求路径 |
request.url 带域名带参数的请求路径 |
request.base_url 带域名请求路径 |
request.url_root 域名 http://127.0.0.1:5000/ |
request.host_url 域名 http://127.0.0.1:5000/ |
request.host 127.0.0.1:5000 |
响应类型
可以直接在视图函数中返回以下四种形式:
- 字符串
- redirect
- render_template
- jsonify
这些会被后续的程序处理为符合传输协议的response,也可以在视图函数中先通过make_response处理为response,然后我们对其返回的response对象中加响应头、cookie等。
session使用
先设置app.secret_key才能使用session
视图中使用session['键']=值
可以存储,使用session.get('键')
可以取到对应的值。
实际上session设置的键值被加密放在了cookie中存在浏览器,而请求过来时携带的cookie会被处理成session对象,可以取出键。这个过程被配置在app.session_interface的类中,分别由open_session,save_session方法实现。
闪现
简单理解为只用一次的session。
flash('%s,传入一个'%name)
,可以设置多次,放在列表中。
flash('超时错误',category="debug")
分类存 (可以按照一定的参数去存)
get_flashed_messages()
取出所有的falsh并删除
实际上也是存到了session中。
ps:以上的内容很多都是前后端混合,了解即可
请求扩展
before_request
请求来了进行处理,如果返回的不是None,则会拦截请求并返回响应。
@app.before_request
def before():
print('我来了111')
@app.before_request
def before():
print('我来了222')
多个函数加装此装饰器,则请求来时按注册顺序执行
after_request
响应走时,进行处理,必须返回响应四件套。
@app.after_request
def after(response):
print('我走了111')
return response
@app.after_request
def after2(response):
print('我走了222')
return response
多个函数加装此装饰器,则请求走时按注册顺序反顺序执行
before_first_request
即将弃用,项目启动后的第一个请求来时做一些处理。
teardown_request
可以拿到视图抛出的异常,在这里做错误日志,而无论视图函数是否出错都会执行,只是e接收到异常与否。
@app.teardown_request
def teardown(e):
print(e)
print('执行我了')
errorhandler
监听响应状态码,如果符合监听的状态码,就会走它,return一个response对象,则会返给前端。
@app.errorhandler(404)
def error_404(arg):
return "404错误了"
@app.errorhandler(500)
def error_500(arg):
return "500错误了"