Flask源码之路由机制(四)

原理

一个 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/', 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))
# 返回 (' -> 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源码之路由机制(四))