03.Flsak源码分析之【路由系统】

3.1 路由加载

路由的两种写法

# 装饰器写法
@app.route('/login')
def login():
    return 'hello world'

# 源码解析后的写法:
def login():
    return 'hello world'
app.add_url_rule('/login', 'login', login)

注册路由规则的时候,flask内部是怎么做的呢?我们来看看 route 方法:

def route(self, rule: str, **options: t.Any) -> t.Callable[[F], F]:
    
    def decorator(f: F) -> F:
        endpoint = options.pop('endpoint', None)
        self.add_url_rule(rule, endpoint, f, **options)
        return f

    return decorator

route 方法内部也是调用 add_url_rule,只不过在外面包了一层装饰器的逻辑,这也验证了上面两种路由的写法本质上是一样的。

NOTE:注意 route 方法是Scaffold(app继承的父类)中的方法。而self.add_url_rule方法中调用的self指向的是app,所以add_url_rule()方法应是app中的,容易看差错的是其父类Scaffold中也有add_url_rule()方法,需要注意不要混淆了。

def add_url_rule(
    self,
    rule: str,
    endpoint: t.Optional[str] = None,
    view_func: t.Optional[t.Callable] = None,
    provide_automatic_options: t.Optional[bool] = None,
    **options: t.Any,
) -> None:
    # 如果没有定义endpoint,默认为函数名
    if endpoint is None:
        endpoint = _endpoint_from_view_func(view_func)  # type: ignore
    options["endpoint"] = endpoint
    methods = options.pop("methods", None)
    
	# 将url和视图函数打包成为rule对象, self.url_rule_class = Rule()
    rule = self.url_rule_class(rule, methods=methods, **options) 
    rule.provide_automatic_options = provide_automatic_options  # type: ignore
	
    # 将rule对象添加到map对象中, self.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"
                f" endpoint function: {endpoint}"
            )
    self.view_functions[endpoint] = view_func

这段代码主要做的事情就是更新 self.url_mapself.view_functions 两个变量。

globals.py中有定义:self.url_map 就是Map()对象,而上面定义的rule就是Rule()对象,view_functions 就是一个字典。

那么路由加载的源码流程可简述为:

  • 将URL和视图函数打包成为 rule 对象
  • 将rule对象添加到map对象中。
  • endpoint(URL别名)和view_func(函数名)组成键值对存入字典view_functions

 
 

3.2 路由分发

上一篇有讲到在请求进来后,会调用full_dispatch_request里面的self.dispatch_request,执行视图函数,这节来详细看看:

def dispatch_request(self) -> ResponseReturnValue:
    # 从_request_ctx_stack栈中获取到当次请求的request
    req = _request_ctx_stack.top.request
    if req.routing_exception is not None:
        # 判断是否异常,如果异常就抛出错误
        self.raise_routing_exception(req)
    # 如果request对象没有异常,就获取url_rule路径
    rule = req.url_rule
    
    if (
        getattr(rule, "provide_automatic_options", False)
        and req.method == "OPTIONS"
    ):# 如果我们提供了provide_automatic_options,并且请求的方法是OPTIONS那么就会执行这里
        return self.make_default_options_response()

    # 从self.view_functions字典中寻找endpoint对应的视图函数,把参数传过去,然后调用它
    return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)

这个方法做的事情就是找到请求对象 request,获取它的 endpoint,然后从 view_functions 找到对应 endpointview_func ,把请求参数传递过去,进行处理并返回。

那么,这里的路由req.url_rule 是怎么匹配的?

在上一篇里提到过创建ctx时,也就是ctx = self.request_context(environ)这行,创建RequestContext对象时,会执行初始化方法。里面一句app.create_url_adapter(self.request)

ctx.py:RequestContext如下:

class RequestContext:
    def __init__(self,app,environ,request=None,session=None):
        self.app = app
        if request is None: # request如果为None,默认会创建一个
            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

app.create_url_adapter(self.request)方法如下:

def create_url_adapter(self, request: t.Optional[Request]) -> t.Optional[MapAdapter]:
    # 由于调用的时候传入了当前的request,所以不为空
    if request is not None:
        # 把app的url_map绑定到WSGI environ变量上
        return self.url_map.bind_to_environ(
            request.environ,
            server_name=self.config["SERVER_NAME"],
            subdomain=subdomain,
        )

    # 如果request为空,但是SERVER_NAME不为空那么也会绑定
    if self.config["SERVER_NAME"] is not None:
        return self.url_map.bind(
            self.config["SERVER_NAME"],
            script_name=self.config["APPLICATION_ROOT"],
            url_scheme=self.config["PREFERRED_URL_SCHEME"],
        )
    # 如果连名字也没有,返回None
    return None

无论是bind_to_environ还是bind,它们的效果相同,最终都会返回一个werkzeug.routing.MapAdapter对象,这个对象主要用于url的匹配,这涉及到werkzeug的源码,这里就不深入研究了,想了解的话建议查阅文档。总之记住,此时ctx.url_adapter不为空,这个很重要。

还是在上一篇ctx.push里,除了创建AppContext对象、将app_ctx对象入栈和将ctx对象入栈之外,最后还做了路由匹配,如下:

def push(self): # self是当前请求的ctx对象,实际也就是RequestContext类的对象

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

此时ctx.url_adapter不为空,所以会调用self.match_request()

def match_request(self) -> None:
    try:
        # 进行路由匹配
        result = self.url_adapter.match(return_rule=True)
        # 获得匹配结果,将路由url_rule和参数view_args保存到request对象里
        self.request.url_rule, self.request.view_args = result
    except HTTPException as e:
        self.request.routing_exception = e

这里调用的是ctx.url_adaptermatch方法,底层是由werkzeug实现的:Map 封装了 Rule 列表,match 的时候会依次调用其中的 rule.match 方法,如果匹配就找到了 match。匹配到的路径保存在了ctx.request对象里,交给dispatch_request,最后匹配到我们写的视图函数。

posted on 2022-06-21 00:41  吃大飞  阅读(79)  评论(0)    收藏  举报