【Flask源码解读】URL 路由

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 初探 看完也够了。

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

响应调用

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

1
2
3
line 1992:  def __call__(self, environ, start_response):

line 1994:      return self.wsgi_app(environ, start_response)

继续看 wsgi_app

1
2
3
4
5
6
7
8
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)

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

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

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

flask/app.py

1
2
3
4
5
6
7
8
9
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

1
2
3
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 对象:

1
2
3
4
5
6
7
8
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

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

· EOF ·

最近爬虫很是凶猛,标注下文章的原文地址: https://liuliqiang.info/post/flask-url-route-explore/