美文网首页
Flask源码之路由机制(四)

Flask源码之路由机制(四)

作者: 看五年前自己的文章真是唏嘘不已 | 来源:发表于2021-05-06 18:31 被阅读0次

    原理

    一个 web 应用中,不同的路径会有不同的处理函数,路由就是根据请求的 URL 找到对应处理函数的过程。

    在下面的例子中,就是根据"/"找到hello_world的过程

    from flask import Flask, request
    
    flask_app = Flask(__name__)
    
    
    @flask_app.route('/',endpoint="11")
    def hello_world():
        return "{}".format(request.remote_addr)
    
    
    if __name__ == '__main__':
        flask_app.run()
    

    我们很容易想到用字典去做路由,keyurlvalue是对应的处理函数或者叫视图函数

    {"/":hello_world}
    

    但是对于动态路由,这样做就不好实现了

    Flask是用MapRule这两种数据结构来实现的,Map的结构类似字典,但能处理更复杂的情况,我们姑且认为Map就是字典

    大概像这样

    {'/': "11"}
    

    value不是视图函数名,而是一个字符串,我们叫它endpoint,除非手动指定,它一般是函数名的字符串形式

    endpoint和视图函数是对应的,这个字典存储在Flaskview_functions属性中,它是一个字典

    它的结构类似这样

    {'11':hello_world}
    
    image-20201230235156471

    以上就是在执行 @flask_app.route('/')的时候发生的事情,Mapview_functions会形成上面的样子

    匹配

    flask有一个url_map属性,这个属性就是Map实例,你可以认为是一个空字典。route的作用就是在项目启动的时候往里面添加Rule对象,就是url规则

    当一个请求来的时候,Flask会根据urlMap找到对应的Rule,再由Rule获取endpoint,再根据endpoint找到对应的function,然后执行 function()

    为什么要endpoint

    直接用视图函数名不好吗?

    有时候我们需要根据视图函数的名字来获取这个视图函数的url

    Flask内置了url_for函数,参数就是endpoint的名字,例如 url_for("11")返回的就是 "/"

    如果没有endpoint而使用函数名的字符串hellow_world,例如url_for("hello_world"),万一你的同事把函数名给改了,你的url_for就要报错了,而endpoint一般不会去改,谁改带他周末去爬山

    参考What is an 'endpoint' in Flask?

    实现

    Rule和Map

    RuleMap都定义在 werkzeug/routing.py

    测试以下代码

    from werkzeug.routing import Map, Rule
    
    m = Map([
        Rule('/', endpoint='index'),
        Rule('/downloads/', endpoint='downloads/index'),
    
    ])
    # 添加一个Rule
    m.add(
        Rule('/downloads/<int:id>', endpoint='downloads/show')
    )
    
    # 把Map绑定到某个域名,返回MapAdapter对象
    urls = m.bind("example.com", "/")
    
    print(urls.match("/downloads/42"))
    # 返回('downloads/show', {'id': 42})
    
    
    print(urls.match("/downloads/42", return_rule=True))
    # 返回 (<Rule '/downloads/<id>' -> downloads/show>, {'id': 42})
    print(urls.match( return_rule=True))
    # 也可以不填原始url字符,直接匹配出当前请求的Rule
    
    

    我们可以知道

    1. Map中的元素是Rule对象,Rule对象其实就是URL规则和endpoint的封装对象
    2. Map要绑定到某个域名下,实际上也可以绑定到environ,毕竟environ中有域名信息
    3. Mapbind方法返回 MapAdapter对象,MapAdapter对象执行实际的匹配工作,它可以根据请求的URL匹配出(Rule,请求参数),也就是match方法做的事情

    route方法

    route方法做的事情就是向Map里面add Rule对象

    我们看route方法执行这一句 @flask_app.route('/',endpoint="11")

        def route(self, rule, **options):
            def decorator(f):
                endpoint = options.pop("endpoint", None)
                # here
                self.add_url_rule(rule, endpoint, f, **options)
                return f
    
            return decorator
    

    再看 add_url_rule

        def add_url_rule(
            self,
            rule,
            endpoint=None,
            view_func=None,
            provide_automatic_options=None,
            **options
        ):
            # 1.没有指定endpoint,那么他就是函数名的字符串形式
            if endpoint is None:
                endpoint = _endpoint_from_view_func(view_func)
            options["endpoint"] = endpoint
            # 2.HTTP请求方法,没有指定那就是GET
            methods = options.pop("methods", None)
            if methods is None:
                methods = getattr(view_func, "methods", None) or ("GET",)
            # 3.把字符串形式的URL规则转换成Rule对象,例如"/"转换成Rule("/")
            rule = self.url_rule_class(rule, methods=methods, **options)
            
            # 4.把Rule添加到Map中,注意Flask对象有一个url_map属性,值一开始就是空的Map对象
            self.url_map.add(rule)
            if view_func is not None:
                old_func = self.view_functions.get(endpoint)
                if old_func is not None and old_func != view_func:
                    raise AssertionError(
                        "View function mapping is overwriting an "
                        "existing endpoint function: %s" % endpoint
                    )
                # 5.把endpoint和视图函数放到view_fuctions这个字典中    
                self.view_functions[endpoint] = view_func
    

    就是为了第4、5两步

    匹配

    匹配的过程就是根据请求的URLmatchRule对象,再根据Rule对象找到视图函数

    full_dispatch_request->dispatch_request->dispatch_request

    def wsgi_app(self, environ, start_response):
        ctx = self.request_context(environ)
        error = None
        try:
            try:
                ctx.push()
                # 看这里
                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
            ctx.auto_pop(error)
    
        def full_dispatch_request(self):
            self.try_trigger_before_first_request_functions()
            try:
                request_started.send(self)
                rv = self.preprocess_request()
                if rv is None:
                    # 看这里,rv就是视图函数的返回结果
                    rv = self.dispatch_request()
            except Exception as e:
                rv = self.handle_user_exception(e)
            return self.finalize_request(rv)
    

    重点看这个方法,请求到这里之后

        def dispatch_request(self):
            req = _request_ctx_stack.top.request
            rule = req.url_rule
            return self.view_functions[rule.endpoint](**req.view_args)
    

    我们知道req是从LocalStack中取出的 RequestContextrequest属性,保存着请求的信息

    request有一个url_rule属性,他就是Rule对象,我们从Rule对象中拿到endpoint,再从 view_functions中根据endpoint拿到视图函数,并传入请求参数,执行之后返回结果就结束了

    一个疑问

    那么req是什么时候有的Rule属性的呢???

    是在 RequestContext对象的push方法中,我们知道请求来了第一步就是push

    我还是删去了一些无关代码

        def push(self):
            if self.url_adapter is not None:
                self.match_request()
    

    match_request做的事情就是用 MapAdapter对象match出当前请求的Rule和请求参数 view_args,然后绑定到request上,这样request就有了url_rule属性

    流程图

    图随手画的,有什么好的画图工具可以推荐一下

    image-20210107170309038.png

    url_adapter就是MapAdapter对象

        def match_request(self):
            try:
                result = self.url_adapter.match(return_rule=True)
                self.request.url_rule, self.request.view_args = result
            except HTTPException as e:
                self.request.routing_exception = e
    

    至于match方法为什么不需要传入当前请求的URL,那是因为url_adapter已经包含了当前请求的信息了

    RequestContext__init__方法中我们可以看到, self.url_adapter = app.create_url_adapter(self.request)

        def __init__(self, app, environ, request=None, session=None):
            self.url_adapter = None
            try:
                self.url_adapter = app.create_url_adapter(self.request)
            except HTTPException as e:
                self.request.routing_exception = e
     
    
    

    再看 create_url_adapter方法,会用Map对象 bind_to_environ

        def create_url_adapter(self, request):
    
            if request is not None:
                # If subdomain matching is disabled (the default), use the
                # default subdomain in all cases. This should be the default
                # in Werkzeug but it currently does not have that feature.
                subdomain = (
                    (self.url_map.default_subdomain or None)
                    if not self.subdomain_matching
                    else None
                )
                return self.url_map.bind_to_environ(
                    request.environ,
                    server_name=self.config["SERVER_NAME"],
                    subdomain=subdomain,
                )
    

    做的事情就是把Map表绑定到environ上,毕竟你这个Map也就是路由表,要属于某个域名

    再看 bind_to_environ这个方法,没必要都看明白,需要的时候,再断点调试就好了

    def bind_to_environ(self, environ, server_name=None, subdomain=None):
      
        def _get_wsgi_string(name):
            val = environ.get(name)
            if val is not None:
                return wsgi_decoding_dance(val, self.charset)
    
        script_name = _get_wsgi_string("SCRIPT_NAME")
        path_info = _get_wsgi_string("PATH_INFO")
        query_args = _get_wsgi_string("QUERY_STRING")
        return Map.bind(
            self,
            server_name,
            script_name,
            subdomain,
            scheme,
            environ["REQUEST_METHOD"],
            #here
            path_info,
            # here
            query_args=query_args,
        )
    

    我们看到,这个方法把从envirion中取出来的path_infoquery_args传到了bind方法中,然后返回MapAdapter对象,接着就可以用MapAdapter对象matchRule

    也就是说我们从 Map构造MapAdapter,然后就可以直接用match方法匹配出当前请求的Rule,再根据Rule获取endpoint,再由此获取视图函数然后调用就好了

    参考文章

    flask 源码解析:路由

    相关文章

      网友评论

          本文标题:Flask源码之路由机制(四)

          本文链接:https://www.haomeiwen.com/subject/gniddltx.html