Flask 的 URL 路由是非常有意思的,不同于 Django 的统一配置,既有 Tornado 般的类路由,更支持大家喜欢的装饰器路由。既然是这么有意思的路由机制,那么肯定大家是很想看看这内部是怎么实现的,那么接下来就一起看看 Flask 是如何实现的。

一个简单的 Flask App

这里先说明一下,本文解析的 Flask 是最新的稳定版 0.12,至于如何弄到源代码,请看后面的附录。既然支持两种指定 URL 方式,那么就先看看大家比较喜欢的 装饰器式路由 吧。源码要从用处出发,就从上一个非常简单的 Flask 应用说起吧:

  1. from flask import Flask
  2. app = Flask(__name__)
  3. @app.route('/')
  4. def hello_world():
  5. return 'Hello, World!'
  6. app.run()

URL 注册

这段代码指定了一个 URL ‘/‘,也就是我们通常访问域名(例如:www.liuliqiang.info) 时所响应的 URL。可以看到,我们指定 URL 的方式是: @app.route('/'),根据 python 的使用经验,我们可以猜测 app.route 是一个带参数的装饰器,那么,我们就去查看代码验证一下,打开 Flask 的源代码: flask/app.py

flask/app.py

  1. line 1054: def route(self, rule, **options):
  2. line 1078: def decorator(f):
  3. endpoint = options.pop('endpoint', None)
  4. self.add_url_rule(rule, endpoint, f, **options)
  5. return f
  6. return decorator

这段代码验证了我们的猜测,这就是一个装饰器,而且是一个带参数的装饰器,对于装饰器不太了解的同学可以看一下我之前写过的一篇文章:Python装饰器浅析,从代码中我们可以看到我们的 URL 是作为参数 rule 传递给了 route,那么我们没有指定其他参数 options,所以在 decorator 函数内部:

变量都搞清楚了,还不知道什么问题的就是这个 add_url_rule 是干什么用的?所以还得跟代码跟进去,这个函数也是在同个文件 flask/app.py

flask/app.py

  1. line 960: @setupmethod
  2. def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
  3. line 1011: if endpoint is None:
  4. endpoint = _endpoint_from_view_func(view_func)
  5. options['endpoint'] = endpoint
  6. line 1043: rule = self.url_rule_class(rule, methods=methods, **options)
  7. self.url_map.add(rule)
  8. line 1047: if view_func is not None:
  9. line 1052: self.view_functions[endpoint] = view_func

这里就不做一些简单的解读了,先说明一下:

这里需要再次提醒一番,因为 Flask 是基于 Werkzeug 构建的,所以强烈建议解读 Flask 源码之前先看看 Werkzeug 这个工具包怎么使用,事实上,我之前写了一篇文章:Werkzeug 初探 看完也够了。

现在我们需要看的是这里是如何将 urlview_func 绑定起来的。其实 Line 1043 就是将 URL 和 endpoint 绑定起来,然后添加到 URL 路由表中,然后在 Line 1052 又将 endpoint响应函数 结合起来,这其实就已经算是注册 URL 完成了。那么,当一个请求进来的时候,我们的响应函数又是怎么被调用的呢?

响应调用

因为 Flask 是一个 WSGI 类 (查看 Flask 的 WSGI 使用方法),所以我们可以关注 __call__ 方法:

  1. line 1992: def __call__(self, environ, start_response):
  2. line 1994: return self.wsgi_app(environ, start_response)

继续看 wsgi_app

  1. line 1952: def wsgi_app(self, environ, start_response):
  2. line 1977: ctx = self.request_context(environ)
  3. ctx.push()
  4. line 1982: response = self.full_dispatch_request()
  5. line 1990: ctx.auto_pop(error)

这里我们有两个点需要特别关注:

  1. ctx 是什么
  2. full_dispatch_request 是什么

我这里选择先忽略 ctx,因为根据名字和场景可以猜测这是一个上下文对象,关注他不外乎是想知道里面有什么内容,所以先没必要看,可以先看 full_dispatch_request:

flask/app.py

  1. line 1600: def full_dispatch_request(self):
  2. line 1610: rv = self.preprocess_request()
  3. if rv is None:
  4. rv = self.dispatch_request()
  5. line 1617: def finalize_request(self, rv, from_error_handler=False):
  6. line 1630: response = self.make_response(rv)
  7. response = self.process_response(response)

这里稍微提一下,但不深入:

这里注重看下 dispatch_request

  1. line 1583: def dispatch_request(self):
  2. line 1596: rule = req.url_rule
  3. line 1603: return self.view_functions[rule.endpoint](**req.view_args)

哈哈,发现了,可以看到,这里根据请求的 URL路径调用了 view_func,也就是我们之前添加的响应函数。

其次再看看 make_response 这个函数其实就是将 view_func 的返回值转换成 Response 对象,因为 Werkzeug 能够处理的响应就是 Response 对象:

  1. line 1690: def make_response(self, rv):
  2. line 1730: if not isinstance(rv, self.response_class):
  3. line 1735: if isinstance(rv, (text_type, bytes, bytearray)):
  4. rv = self.response_class(rv, headers=headers,
  5. status=status_or_headers)
  6. headers = status_or_headers = None
  7. else:
  8. rv = self.response_class.force_type(rv, request.environ)

这里的 response_class 就是: werkzeug.wrappers.Response 的子类:

flask/wrappers.py

  1. line 194: class Response(ResponseBase):

ok,到这里这部分的代码算是都解析完了,但是,这里梳理的都还只是粗粒度的流程,还有很多细节是我们需要去关注的,所以希望阅读这篇文章的同学可以自己亲自去看看。

总结

从上面的解读来看, Flask 处理 URL 的流程可以总结成这样:

  1. 将 URL 和 endpoint 绑定
  2. endpoint 和 响应函数绑定
  3. 当请求过来时,先使用 Werkzeug 预处理请求数据,然后更具 endpoint 调用 响应函数
  4. 将 响应函数 结果封装成 WerkzeugResponse 对象
  5. 完成请求

Reference

  1. Flask 0.12 Docs
  2. Werkzeug 0.11 Docs