Flask 的 URL 路由是非常有意思的,不同于 Django 的统一配置,既有 Tornado 般的类路由,更支持大家喜欢的装饰器路由。既然是这么有意思的路由机制,那么肯定大家是很想看看这内部是怎么实现的,那么接下来就一起看看 Flask 是如何实现的。
一个简单的 Flask App
这里先说明一下,本文解析的 Flask 是最新的稳定版 0.12,至于如何弄到源代码,请看后面的附录。既然支持两种指定 URL 方式,那么就先看看大家比较喜欢的 装饰器式路由 吧。源码要从用处出发,就从上一个非常简单的 Flask 应用说起吧:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello, World!'
app.run()
URL 注册
这段代码指定了一个 URL ‘/‘,也就是我们通常访问域名(例如:www.liuliqiang.info) 时所响应的 URL。可以看到,我们指定 URL 的方式是: @app.route('/')
,根据 python 的使用经验,我们可以猜测 app.route
是一个带参数的装饰器,那么,我们就去查看代码验证一下,打开 Flask 的源代码: flask/app.py
flask/app.py
line 1054: def route(self, rule, **options):
line 1078: def decorator(f):
endpoint = options.pop('endpoint', None)
self.add_url_rule(rule, endpoint, f, **options)
return f
return decorator
这段代码验证了我们的猜测,这就是一个装饰器,而且是一个带参数的装饰器,对于装饰器不太了解的同学可以看一下我之前写过的一篇文章:Python装饰器浅析,从代码中我们可以看到我们的 URL 是作为参数 rule
传递给了 route
,那么我们没有指定其他参数 options
,所以在 decorator
函数内部:
- f: 对应于我们的 URL 处理函数
hello_world
- endpoint: 因为 options 是 {},所以 endpoint 是 None
- rule: 对应于我们传进来的 URL,所以 rule 是
'/'
变量都搞清楚了,还不知道什么问题的就是这个 add_url_rule
是干什么用的?所以还得跟代码跟进去,这个函数也是在同个文件 flask/app.py
:
flask/app.py
line 960: @setupmethod
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
line 1011: if endpoint is None:
endpoint = _endpoint_from_view_func(view_func)
options['endpoint'] = endpoint
line 1043: rule = self.url_rule_class(rule, methods=methods, **options)
self.url_map.add(rule)
line 1047: if view_func is not None:
line 1052: self.view_functions[endpoint] = view_func
这里就不做一些简单的解读了,先说明一下:
self.url_rule_class
其实就是:werkzeug.routing.Rule
已经到了 Werkzeug 地盘,可以pass了。
这里需要再次提醒一番,因为 Flask 是基于 Werkzeug 构建的,所以强烈建议解读 Flask 源码之前先看看 Werkzeug
这个工具包怎么使用,事实上,我之前写了一篇文章:Werkzeug 初探 看完也够了。
现在我们需要看的是这里是如何将 url
和 view_func
绑定起来的。其实 Line 1043 就是将 URL 和 endpoint 绑定起来,然后添加到 URL 路由表中,然后在 Line 1052 又将 endpoint 和 响应函数 结合起来,这其实就已经算是注册 URL 完成了。那么,当一个请求进来的时候,我们的响应函数又是怎么被调用的呢?
响应调用
因为 Flask 是一个 WSGI 类 (查看 Flask 的 WSGI 使用方法),所以我们可以关注 __call__
方法:
line 1992: def __call__(self, environ, start_response):
line 1994: return self.wsgi_app(environ, start_response)
继续看 wsgi_app
line 1952: def wsgi_app(self, environ, start_response):
line 1977: ctx = self.request_context(environ)
ctx.push()
line 1982: response = self.full_dispatch_request()
line 1990: ctx.auto_pop(error)
这里我们有两个点需要特别关注:
ctx
是什么full_dispatch_request
是什么
我这里选择先忽略 ctx
,因为根据名字和场景可以猜测这是一个上下文对象,关注他不外乎是想知道里面有什么内容,所以先没必要看,可以先看 full_dispatch_request
:
flask/app.py
line 1600: def full_dispatch_request(self):
line 1610: rv = self.preprocess_request()
if rv is None:
rv = self.dispatch_request()
line 1617: def finalize_request(self, rv, from_error_handler=False):
line 1630: response = self.make_response(rv)
response = self.process_response(response)
这里稍微提一下,但不深入:
preprocess_request
: 执行before_request()
装饰器装饰的函数process_response
: 执行after_request()
装饰器装饰的函数
这里注重看下 dispatch_request
line 1583: def dispatch_request(self):
line 1596: rule = req.url_rule
line 1603: return self.view_functions[rule.endpoint](**req.view_args)
哈哈,发现了,可以看到,这里根据请求的 URL路径调用了 view_func
,也就是我们之前添加的响应函数。
其次再看看 make_response
这个函数其实就是将 view_func
的返回值转换成 Response
对象,因为 Werkzeug 能够处理的响应就是 Response
对象:
line 1690: def make_response(self, rv):
line 1730: if not isinstance(rv, self.response_class):
line 1735: if isinstance(rv, (text_type, bytes, bytearray)):
rv = self.response_class(rv, headers=headers,
status=status_or_headers)
headers = status_or_headers = None
else:
rv = self.response_class.force_type(rv, request.environ)
这里的 response_class
就是: werkzeug.wrappers.Response
的子类:
flask/wrappers.py
line 194: class Response(ResponseBase):
ok,到这里这部分的代码算是都解析完了,但是,这里梳理的都还只是粗粒度的流程,还有很多细节是我们需要去关注的,所以希望阅读这篇文章的同学可以自己亲自去看看。
总结
从上面的解读来看, Flask 处理 URL 的流程可以总结成这样:
- 将 URL 和
endpoint
绑定 - 将
endpoint
和 响应函数绑定 - 当请求过来时,先使用 Werkzeug 预处理请求数据,然后更具
endpoint
调用 响应函数 - 将 响应函数 结果封装成 Werkzeug 的
Response
对象 - 完成请求