原理
一个 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()
我们很容易想到用字典去做路由,key
是 url
,value
是对应的处理函数或者叫视图函数
{"/":hello_world}
但是对于动态路由,这样做就不好实现了
Flask
是用Map
和Rule
这两种数据结构来实现的,Map
的结构类似字典,但能处理更复杂的情况,我们姑且认为Map
就是字典
大概像这样
{'/': "11"}
value
不是视图函数名,而是一个字符串,我们叫它endpoint
,除非手动指定,它一般是函数名的字符串形式
endpoint
和视图函数是对应的,这个字典存储在Flask
的view_functions
属性中,它是一个字典
它的结构类似这样
{'11':hello_world}
image-20201230235156471
以上就是在执行 @flask_app.route('/')
的时候发生的事情,Map
和view_functions
会形成上面的样子
匹配
flask
有一个url_map
属性,这个属性就是Map
实例,你可以认为是一个空字典。route
的作用就是在项目启动的时候往里面添加Rule
对象,就是url
规则
当一个请求来的时候,Flask
会根据url
在Map
找到对应的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
Rule
和Map
都定义在 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
我们可以知道
-
Map
中的元素是Rule
对象,Rule
对象其实就是URL
规则和endpoint
的封装对象 -
Map
要绑定到某个域名下,实际上也可以绑定到environ
,毕竟environ
中有域名信息 -
Map
的bind
方法返回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两步
匹配
匹配的过程就是根据请求的URL
去match
出Rule
对象,再根据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
中取出的 RequestContext
的request
属性,保存着请求的信息
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.pngurl_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_info
和query_args
传到了bind
方法中,然后返回MapAdapter
对象,接着就可以用MapAdapter
对象match
出Rule
了
也就是说我们从 Map
构造MapAdapter
,然后就可以直接用match
方法匹配出当前请求的Rule
,再根据Rule
获取endpoint
,再由此获取视图函数然后调用就好了
网友评论