[Python自学] Flask框架 (3) (路由、CBV、自定义正则动态路由、请求处理流程、蓝图)
一、路由系统
1.浅析@app.route的源码
我们使用@app.route("/index")可以给视图函数加上路由映射。我们分析一下@app.route装饰器的实现源码:
def route(self, rule, **options): def decorator(f): endpoint = options.pop("endpoint", None) self.add_url_rule(rule, endpoint, f, **options) return f return decorator
可以看到,装饰器的核心就是add_url_rule()函数。这里的self就是Flask的实例app。因为是app调用的route。
也就是说,我们不使用装饰器,也可以直接调用该函数实现路由映射:
def test(): return 'test' # 使用app.add_url_rule代替@app.route # 第一个参数就是url,第二个参数是endpoint(即路由name),第三个参数为视图函数引用 app.add_url_rule('/test', None, test)
执行结果:
可以看到,这种方式实现的路由,也可以正常访问。
2.分析add_url_rule函数
@setupmethod def add_url_rule( self, rule, endpoint=None, view_func=None, provide_automatic_options=None, **options ): if endpoint is None: # 如果传入的endpoint为None,则使用视图函数的__name__作为endpoint endpoint = _endpoint_from_view_func(view_func) options["endpoint"] = endpoint # 将endpoint设置到options中 methods = options.pop("methods", None) # 从参数中获取methods,如果没有,则为None # if the methods are not given and the view_func object knows its # methods we can use that instead. If neither exists, we go with # a tuple of only ``GET`` as default. if methods is None: # 如果methods为None,就去view_func中找methods,如果找不到,则默认为GET methods = getattr(view_func, "methods", None) or ("GET",) if isinstance(methods, string_types): # 如果methods是str,则报错,必须是列表 raise TypeError( "Allowed methods have to be iterables of strings, " 'for example: @app.route(..., methods=["POST"])' ) methods = set(item.upper() for item in methods) # methods中元素全部转换为大写 # Methods that should always be added required_methods = set(getattr(view_func, "required_methods", ())) # 获取required_methods # starting with Flask 0.8 the view_func object can disable and # force-enable the automatic options handling. if provide_automatic_options is None: # 获取provide_automatic_options provide_automatic_options = getattr( view_func, "provide_automatic_options", None ) if provide_automatic_options is None: # 如果还为None if "OPTIONS" not in methods: # 如果methods中没有OPTIONS provide_automatic_options = True # provide_automatic_options置为True required_methods.add("OPTIONS") else: provide_automatic_options = False # 如果methods中有,则provide_automatic_options置为False # Add the required methods now. methods |= required_methods # 并集 # 将我们传入的url,endpoint,func等封装起来,成为一个Rule对象 rule = self.url_rule_class(rule, methods=methods, **options) rule.provide_automatic_options = provide_automatic_options # 将封装好的Rule对象添加到Map类的对象中 self.url_map.add(rule) if view_func is not None: # 传入的视图函数是否为空,这里不为空 old_func = self.view_functions.get(endpoint) # 去view_functions字典中看看有没有同名的视图函数 if old_func is not None and old_func != view_func: # 如果有同名视图函数,且函数不是我们当前传入的函数,则报错 raise AssertionError( "View function mapping is overwriting an " "existing endpoint function: %s" % endpoint # 报错:存在同名的视图函数 ) # 如果endpoint没有冲突,则将视图函数加入view_functions字典中 self.view_functions[endpoint] = view_fun
这段代码主要的功能就是,将我们传入的url、endpoint、methods等一系列路由参数,封装成一个Rule对象,然后添加到Map对象中。然后判断是否存在endpoint冲突的视图函数,如果没有,则将 endpoint:视图函数引用 键值对存放在app.view_functions字典中,该字典主要就是用来检查endpoint的冲突问题。
所以从这里可以看出,我们在写路由的时候,尽量不要让endpoint重名,如果一定要重名,则函数必须是相同的(例如两个url对应一个视图函数的场景)。例如:
@app.route('/test2', endpoint='t1') @app.route('/test', endpoint='t1') def test(): return 'test'
3.@app.route装饰器的参数
@app.route和app.add_url_rule参数: rule, # URL规则 view_func, # 视图函数名称 defaults = None, # 默认值, 当URL中无参数,函数需要参数时,使用defaults = {'k': 'v'}为函数提供参数 endpoint = None, # 名称,用于反向生成URL,即: url_for('名称') methods = None, # 允许的请求方式,如:["GET", "POST"] strict_slashes = None, # 对URL最后的 / 符号是否严格要求,如: 例如: @app.route('/index', strict_slashes=False) #访问 http:// www.xx.com/index/ 或http://www.xx.com/index 均可 @app.route('/index', strict_slashes=True) #仅访问 http://www.xx.com/index indexredirect_to = None, # 重定向到指定地址 如: 例如: @app.route('/index/<int:nid>', redirect_to='/home/<nid>') 或 def func(adapter, nid): return "/home/888" @app.route('/index/<int:nid>', redirect_to=func) subdomain = None, # 子域名访问,什么是子域名:主干域名是www.leeoo.com admin.leeoo.com就是admin子域名 例如: from flask import Flask, views, url_for app = Flask(import_name=__name__) # 配置服务器地址和端口 app.config['SERVER_NAME'] = 'leeoo.com:5000' # 当访问admin.leeoo.com/时才会走这个路由 @app.route("/", subdomain="admin") def static_index(): """Flask supports static subdomains This is available at static.your-domain.tld""" return "static.your-domain.tld" # 动态子域名,访问user1.leeoo.com/dynamic,则相当于将'user1'作为参数传入username_index()视图函数 @app.route("/dynamic", subdomain="<username>") def username_index(username): """Dynamic subdomains are also supported Try going to user1.your-domain.tld/dynamic""" return username + ".your-domain.tld" if __name__ == '__main__': app.run()
二、CBV
1.Flask中的CBV
在Flask中也可以使用类似Django的CBV。
from flask import views class UserView(views.MethodView): def get(self, *args, **kwargs): return 'GET' def post(self, *args, **kwargs): return 'POST' # CBV不能使用装饰器添加路由,只能使用app.add_url_rule(),注意as_view()的参数会被传递给view_func.__name__,然后会赋值给endpoint app.add_url_rule('/user', None, UserView.as_view("userview"))
Flask的CBV和django的很类似。当用户的请求到达时,通过MethodView类的dispatch_request()方法,来反射到对应的get或post等视图函数。如下源码所示:
def dispatch_request(self, *args, **kwargs): # 用户请求类型request.method先转化为小写,然后看视图类中是否存在对应的方法 meth = getattr(self, request.method.lower(), None) # If the request method is HEAD and we don't have a handler for it # retry with GET. if meth is None and request.method == "HEAD": meth = getattr(self, "get", None) # 如果meth为None,则说明用户请求类型没有对应的处理函数,报错 assert meth is not None, "Unimplemented method %r" % request.method # 否则调用对应视图函数 return meth(*args, **kwargs)
2.视图类的静态属性
from flask import views # 实现一个自定义装饰器 def wrapper(func): def inner(*args, **kwargs): return func(*args, **kwargs) return inner class UserView(views.MethodView): methods = ['GET'] # 限制支持的请求类型 decorators = [wrapper, ] # 在这里使用自定义装饰器,会自动批量添加到各个视图函数 def get(self, *args, **kwargs): return 'GET' def post(self, *args, **kwargs): return 'POST' app.add_url_rule('/user', None, UserView.as_view("userview"))
我们可以定义静态属性methods来限制该视图类接收的请求类型。可以定义decorators来批量的对类中的视图函数(get、post...函数)添加自定义装饰器(当然也可以自己手动给需要的视图函数添加)。
三、自定义支持正则的动态路由
我们在使用Flask的动态路由时,Flask默认为我们提供了几种数据类型。参考:[Python自学] Flask框架 (1) (Flask介绍、配置、Session、路由、请求和响应、Jinjia2模板语言、视图装饰器)
1.Flask默认支持的动态参数数据类型
我们可以在app.url_map.converters中看到Flask默认支持的数据类型:
#: the default converter mapping for the map. DEFAULT_CONVERTERS = { "default": UnicodeConverter, "string": UnicodeConverter, "any": AnyConverter, "path": PathConverter, "int": IntegerConverter, "float": FloatConverter, "uuid": UUIDConverter, }
该字典中,key为支持的类型名,value即为提供转换功能的转换器。
如果我们想要Flask的动态路由支持正则表达式,则需要自己定义一个正则转换器,并添加到app.url_map.converters中。
2.自定义正则转换器
from werkzeug.routing import BaseConverter class RegexConverter(BaseConverter): """ 自定义URL匹配正则表达式 """ def __init__(self, map, regex): super(RegexConverter, self).__init__(map) self.regex = regex def to_python(self, value): """ 路由匹配时,匹配成功后传递给视图函数中参数的值 :param value: :return: """ return value def to_url(self, value): """ 使用url_for反向生成URL时,传递的参数经过该方法处理,返回的值用于生成URL中的参数 :param value: :return: """ val = super(RegexConverter, self).to_url(value) return val # 添加到flask中 app.url_map.converters['regex'] = RegexConverter @app.route('/index/<regex("\d+-\d+"):nid>') def index(nid): print(url_for('index', nid='888-999')) return 'Index'
这样,我们就可以在动态路由中,使用正则表达式了。但是注意,这里传递进来的nid是字符串格式(我们也可以在RegexConverter类的to_python中对其进行处理)。
四、Flask请求处理流程
1.启动服务器
我们知道,最简单的Flask代码如下:
from flask import Flask app = Flask(__name__) if __name__ == '__main__': app.run()
Flask是建立在 werkzeug 这个WSGI服务器上的。
当app.run()运行Flask的时候,底层的werkzeug会开始监听指定的端口,准备接受用户请求。
我们可以在Flask类中的run方法找到如下代码:
try: run_simple(host, port, self, **options)
这个run_simple的第三个参数就是满足WSGI协议调用的方法。这里传入了self,这个self就是代指app对象自己。所以当服务器接收到请求时,会调用app(),其实就是调用app中的__call__()方法。
2.接收请求
我们看Flask类中__call__的源代码:
def __call__(self, environ, start_response): """The WSGI server calls the Flask application object as the WSGI application. This calls :meth:`wsgi_app` which can be wrapped to applying middleware.""" return self.wsgi_app(environ, start_response)
这个__call__方法是在请求到达的时候才会被调用。而被调用时参数是由werkzeug服务器传入的,其中environ是请求相关的信息,而start_response是服务器提供给Flask框架用来封装响应头的函数引用。
可以参考:[Python之路] 实现简易HTTP服务器与MINI WEB框架(利用WSGI实现服务器与框架解耦)中WSGI原理。
3.处理请求和Session
所有Flask框架的源码都是从wsgi_app()这个函数开始的:
def wsgi_app(self, environ, start_response): # 1.ctx = RequestContext(self, environ) # ctx.request = Request(environ) # ctx.session = None ctx = self.request_context(environ) error = None try: try: # 2.将ctx对象加入上下文管理, # 3.执行 SecureCookieSessioninterface.open_session,去cookie中获取session值,并给ctx.session重新赋值 ctx.push() # 4.这里调用视图函数 # app.dispatch_request()调用视图函数 # 5.视图函数执行完毕后,调用app.finalize_request(),进行善后工作 # 在finalize_request中调用process_response,将用户新设置的session加密序列化后写入response中,这里调用的是SecureCookieSessioninterface.save_session response = self.full_dispatch_request() except Exception as e: error = e response = self.handle_exception(e) except: # noqa: B001 error = sys.exc_info()[1] raise return response(environ, start_response) finally: if self.should_ignore_error(error): error = None # 5.视图函数处理完请求,返回了响应之后,清空该次请求在上下文中的数据 ctx.auto_pop(error)
ctx是app.request_context(environ)中返回的RequestContext实例,并将self和environ传递进去:
def request_context(self, environ): # 实例化RequestContext,传入app和environ return RequestContext(self, environ)
再看RequestContext类的构造函数:
def __init__(self, app, environ, request=None, session=None): # Flask实例app self.app = app # 这里request我们没有传入,一定为空 if request is None: # request_class是Request类,所以request是Request类的一个实例,并封装了environ(得到我们使用的request) request = app.request_class(environ) self.request = request self.url_adapter = None try: self.url_adapter = app.create_url_adapter(self.request) except HTTPException as e: self.request.routing_exception = e # 闪现初始化为None self.flashes = None # session初始化为None self.session = session self._implicit_app_ctx_stack = [] self.preserved = False self._preserved_exc = None self._after_request_functions = []
4.请求处理流程图
五、蓝图
1.修改Flask项目目录结构
在划分目录之前,我们的static目录、templates目录以及写视图函数的app.py文件都位于项目根目录下。
我们对目录进行以下修改:
1)项目根目录下创建项目同名目录my_flask目录,以及manage.py文件。以后我们的Flask项目就从manage.py启动,而不是以前的app.py
2)在创建好的my_flask目录下,创建static、templates、views目录,以及__init__.py文件。其中static存放静态文件,templates存放模板、view存放视图函数py文件,__init__.py中会创建app实例(Flask对象)。
2.实现my_flask目录下的__init__.py
from flask import Flask # 封装一个函数,用来生成Flask实例,并返回 def create_app(): app = Flask(__name__) return app
3.实现根目录下的manage.py
# 从my_flask包中导入create_app函数 from my_flask import create_app # 创建app实例 app = create_app() if __name__ == '__main__': # 运行Flask app.run()
manage.py作为整个Flask项目的入口。
4.视图函数分类
我们的视图函数都应该放在views目录下,可以对其进行分门别类,例如登录类的视图函数在login.py中实现,用户信息类的视图函数在user.py中实现。
login.py和user.py的实现:
# login.py文件 # 导入蓝图模块 from flask import Blueprint, render_template # 定义一个蓝图对象 lg = Blueprint('lg', __name__) # 使用蓝图来调用装饰器(而不是使用app) @lg.route('/login') def login(): return render_template('login.html', msg="这是Login页面")
# user.py文件 from flask import Blueprint, render_template us = Blueprint('us', __name__, template_folder='./templates') @us.route('/user_list') def user_list(): return render_template('user_list.html', msg="这里是USER LIST")
我们通过在每个视图实现文件中都定义一个蓝图实例,然后利用蓝图实例来调用route装饰器给视图函数添加路由映射。
但是,使用了蓝图实例后,还需要将蓝图和app实例建立关系:
# my_flask/__init__.py文件 from flask import Flask from .views.login import lg from .views.user import us def create_app(): app = Flask(__name__) app.register_blueprint(lg) app.register_blueprint(us) return app
这样,我们就成功的利用蓝图实现了视图函数的分类,让我们的项目更清晰明了。
5.蓝图指定模板目录
我们看到,在4.节的user.py中,蓝图的参数多了一个template_folder='./templates'。
这个参数的意思是,该蓝图可以单独指定自己的视图函数中的render_template使用的模板在哪里查找。
但是,需要注意的是,即使设置了template_folder参数,render_template也会先去全局的templates目录中查找,如果没有对应的模板,才会去蓝图中指定的目录中寻找。
我们调整目录结构:
位于项目同名目录my_flask下的templates为全局模板目录。这个目录是所有视图函数默认优先查找的目录。
而views中的templates目录,是我们另外任意创建的一个目录(目录名不一定叫templates)。这个目录可以被views中py文件中定义的蓝图所指定。
如下代码所示:
# user.py from flask import Blueprint, render_template us = Blueprint('us', __name__, template_folder='./templates') @us.route('/user_list') def user_list(): return render_template('user_list.html', msg="这里是USER LIST")
user.py中定义蓝图的时候,指定了views/templates目录。
当我们访问/user_list页面的时候,render_template函数会先去全局的templates目录中查找user_list.html模板。结果为:
而当我们删除全局templates中的user_list.html文件,只留下views/templates中的user_list.html文件,结果变为:
优先级总结:全局模板目录 > 蓝图实例化指定的模板目录
6.蓝图路由前缀
在创建蓝图实例时,可以为其相关的所有路由设置一个前缀:
us = Blueprint('us', __name__, template_folder='./templates',url_prefix='/user') @us.route('/user_list') def user_list(): return render_template('user_list.html', msg="这里是USER LIST")
这里我们使用url_prefix参数设置了一个前缀"/user"。
以后我们要访问/user_list页面的时候,就需要在url中多加一层"/user":
# 原本的访问URL http://127.0.0.1:5000/user_list # 加了前缀后的访问URL http://127.0.0.1:5000/user/user_list
只要使用该蓝图实例来调用装饰器的路由映射,都需要加上该前缀。这种功能有点类似于django中的路由分发,但是又更加的灵活。
##