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_map 和 self.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 找到对应 endpoint 的 view_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_adapter的match方法,底层是由werkzeug实现的:Map 封装了 Rule 列表,match 的时候会依次调用其中的 rule.match 方法,如果匹配就找到了 match。匹配到的路径保存在了ctx.request对象里,交给dispatch_request,最后匹配到我们写的视图函数。
浙公网安备 33010602011771号