专栏名称: 编程派
Python程序员都在看的公众号,跟着编程派一起学习Python,看最新国外教程和资源!
目录
相关文章推荐
Python爱好者社区  ·  yyds!论文教程 ·  5 天前  
Python爱好者社区  ·  论文入门手册 ·  3 天前  
Python爱好者社区  ·  华为校招开了侮辱价。 ·  4 天前  
Python爱好者社区  ·  上海微软大裁员,赔偿达N+8,老员工获赔77万! ·  3 天前  
Python爱好者社区  ·  你觉得是上班更苦还是上学更苦? ·  5 天前  
51好读  ›  专栏  ›  编程派

Flask 源码解析:路由

编程派  · 公众号  · Python  · 2017-05-12 11:40

正文

原文:http://cizixs.com/2017/01/12/flask-insight-routing

全文约 8600 字,读完可能需要 13 分钟。

这是 flask 源码解析系列文章的其中一篇,本系列已发文章列表:

构建路由规则

一个 web 应用不同的路径会有不同的处理函数,路由就是根据请求的 URL 找到对应处理函数的过程。

在执行查找之前,需要有一个规则列表,它存储了 url 和处理函数的对应关系。最容易想到的解决方案就是定义一个字典,key 是 url,value 是对应的处理函数。如果 url 都是静态的(url 路径都是实现确定的,没有变量和正则匹配),那么路由的过程就是从字典中通过 url 这个 key ,找到并返回对应的 value;如果没有找到,就报 404 错误。而对于动态路由,还需要更复杂的匹配逻辑。

flask 中的路由过程是这样的吗?这篇文章就来分析分析。

在分析路由匹配过程之前,我们先来看看 flask 中, 构建这个路由规则 的两种方法:

  1. 通过 @app.route() decorator,比如文章开头给出的 hello world 例子

  2. 通过 app.add_url_rule,这个方法的签名为 add_url_rule(self, rule, endpoint=None,view_func=None, **options),参数的含义如下:

  • rule: url 规则字符串,可以是静态的 /path,也可以包含 /

  • endpoint:要注册规则的 endpoint,默认是 view_func 的名字

  • view_func:对应 url 的处理函数,也被称为视图函数

这两种方法是等价的,也就是说:

  1. @app.route('/')

  2. def hello():

  3.    return "hello, world!"

也可以写成

  1. def hello():

  2.    return "hello, world!"

  3. app.add_url_rule('/', 'hello', hello)

NOTE: 其实,还有一种方法来构建路由规则----直接操作 app.url_map 这个数据结构。不过这种方法并不是很常用,因此就不展开了。

注册路由规则的时候,flask 内部做了哪些东西呢?我们来看看 route 方法:

  1. def route(self, rule, **options):

  2.    """A decorator that is used to register a view function for a

  3.    given URL rule.  This does the same thing as :meth:`add_url_rule`

  4.    but is intended for decorator usage.

  5.    """

  6.    def decorator(f):

  7.        endpoint = options.pop('endpoint', None)

  8.        self.add_url_rule(rule, endpoint, f, **options)

  9.        return f

  10.    return decorator

route 方法内部也是调用 add_url_rule,只不过在外面包了一层装饰器的逻辑,这也验证了上面两种方法等价的说法。

  1. def add_url_rule(self, rule, endpoint=None, view_func=None, **options):

  2.    """Connects a URL rule.  Works exactly like the :meth:`route`

  3.    decorator.  If a view_func is provided it will be registered with the

  4.    endpoint.

  5.    """

  6.    methods = options.pop('methods', None)

  7.    rule = self.url_rule_class(rule, methods=methods, **options)

  8.    self.url_map.add(rule)

  9.    if view_func is not None:

  10.        old_func = self.view_functions.get(endpoint)

  11.        if old_func is not None and old_func != view_func:

  12.            raise AssertionError('View function mapping is overwriting an '

  13.                                 'existing endpoint function: %s' % endpoint)

  14.        self.view_functions[endpoint] = view_func

上面这段代码省略了处理 endpoint 和构建 methods 的部分逻辑,可以看到它主要做的事情就是更新 self.url_map 和 self.view_functions 两个变量。找到变量的定义,发现 url_map 是 werkzeug.routeing:Map 类的对象, rule 是 werkzeug.routing:Rule 类的对象, view_functions就是一个字典。这和我们之前预想的并不一样,这里增加了 Rule 和 Map 的封装,还把 url 和 view_func 保存到了不同的地方。

需要注意的是:每个视图函数的 endpoint 必须是不同的,否则会报 AssertionError

werkzeug 路由逻辑

事实上,flask 核心的路由逻辑是在 werkzeug 中实现的。所以在继续分析之前,我们先看一下 werkzeug 提供的 路由功能

  1. >>> m = Map([

  2. ...     Rule('/', endpoint='index'),

  3. ...     Rule('/downloads/', endpoint='downloads/index'),

  4. ...     Rule('/downloads/', endpoint='downloads/show')

  5. ... ])

  6. >>> urls = m.bind("example.com", "/")

  7. >>> urls.match("/", "GET")

  8. ('index', {})

  9. >>> urls.match("/downloads/42")

  10. ('downloads/show', {'id': 42})

  11. >>> urls.match("/downloads")

  12. Traceback (most recent call last):

  13.  ...

  14. RequestRedirect: http://example.com/downloads/

  15. >>> urls.match("/missing")

  16. Traceback (most recent call last):

  17.  ...

  18. NotFound: 404 Not Found

上面的代码演示了 werkzeug 最核心的路由功能:添加路由规则(也可以使用 m.add),把路由表绑定到特定的环境( m.bind),匹配 url( urls.match)。正常情况下返回对应的 endpoint 名字和参数字典,可能报重定向或者 404 异常。

可以发现, endpoint 在路由过程中非常重要。 werkzeug 的路由过程,其实是 url 到 endpoint 的转换:通过 url 找到处理该 url 的 endpoint。至于 endpoint 和 view function 之间的匹配关系, werkzeug 是不管的,而上面也看到 flask 是把这个存放到字典中的。

flask 路由实现

好,有了这些基础知识,我们回头看 dispatch_request,继续探寻路由匹配的逻辑:

  1. def dispatch_request(self):

  2.    """Does the request dispatching.  Matches the URL and returns the

  3.    return value of the view or error handler.  This does not have to

  4.    be a response object.  In order to convert the return value to a

  5.    proper response object, call :func:`make_response`.

  6.    """

  7.    req = _request_ctx_stack.top.request

  8.    if req.routing_exception is not None:

  9.        self.raise_routing_exception(req)

  10.    rule = req.url_rule

  11.    # dispatch to the handler for that endpoint

  12.    return self.view_functions[rule.endpoint](**req.view_args)

这个方法做的事情就是找到请求对象 request,获取它的 endpoint,然后从 view_functions 找到对应 endpoint 的 view_func ,把请求参数传递过去,进行处理并返回。 view_functions 中的内容,我们已经看到,是在构建路由规则的时候保存进去的;那请求中 req.url_rule 是什么保存进去的呢?它的格式又是什么?

我们可以先这样理解: _request_ctx_stack.top.request 保存着当前请求的信息,在每次请求过来的时候, flask 会把当前请求的信息保存进去,这样我们就能在整个请求处理过程中使用它。至于怎么做到并发情况下信息不会相互干扰错乱,我们将在下一篇文章介绍。

_request_ctx_stack 中保存的是 RequestContext 对象,它出现在 flask/ctx.py 文件中,和路由相关的逻辑如下:

  1. class RequestContext(object):

  2.    def __init__(self, app, environ, request=None):

  3.        self.app = app

  4.        self.request = request

  5.        self.url_adapter = app.create_url_adapter(self.request)

  6.        self.match_request()

  7.    def match_request(self):

  8.        """Can be overridden by a subclass to hook into the matching

  9.        of the request.

  10.        """

  11.        try:

  12.            url_rule, self.request.view_args = \

  13.                self.url_adapter.match(return_rule=True)

  14.            self.request.url_rule = url_rule

  15.        except HTTPException as e:

  16.            self.request.routing_exception = e

  17. class Flask(_PackageBoundObject):

  18.    def create_url_adapter(self, request):

  19.        """Creates a URL adapter for the given request.  The URL adapter

  20.        is created at a point where the request context is not yet set up

  21.        so the request is passed explicitly.

  22.        """

  23.        if request is not None:

  24.            return self.url_map.bind_to_environ(request.environ,

  25.                server_name=self.config['SERVER_NAME'])

在初始化的时候,会调用 app.create_url_adapter 方法,把 app 的 url_map 绑定到 WSGI environ 变量上( bind_to_environ 和之前的 bind 方法作用相同)。最后会调用 match_request方法,这个方式调用了 url_adapter.match 方法,进行实际的匹配工作,返回匹配的 url rule。而我们之前使用的 url_rule.endpoint 就是匹配的 endpoint 值。

整个 flask 的路由过程就结束了,总结一下大致的流程:

  • 通过 @app.route 或者 app.add_url_rule 注册应用 url 对应的处理函数

  • 每次请求过来的时候,会事先调用路由匹配的逻辑,把路由结果保存起来

  • dispatch_request 根据保存的路由结果,调用对应的视图函数

match 实现

虽然讲完了 flask 的路由流程,但是还没有讲到最核心的问题: werkzeug 中是怎么实现 match 方法的。 Map 保存了 Rule 列表, match 的时候会依次调用其中的 rule.match 方法,如果匹配就找到了 match。 Rule.match 方法的代码如下:

  1. def match(self, path):

  2.        """Check if the rule matches a given path. Path is a string in the

  3.        form ``"subdomain|/path(method)"`` and is assembled by the map.  If

  4.        the map is doing host matching the subdomain part will be the host

  5.        instead.

  6.        If the rule matches a dict with the converted values is returned,

  7.        otherwise the return value is `None`.

  8.        """

  9.        if not self.build_only:

  10.            m = self._regex.search(path)

  11.            if m is not None:

  12.                groups = m.groupdict()

  13.                result = {}

  14.                for name, value in iteritems(groups):

  15.                    try:

  16.                        value = self._converters[name].to_python(value)

  17.                    except ValidationError:

  18.                        return

  19.                    result[str(name)] = value

  20.                if self.defaults:

  21.                    result.update(self.defaults)

  22.                return result

它的逻辑是这样的:用实现 compile 的正则表达式去匹配给出的真实路径信息,把所有的匹配组件转换成对应的值,保存在字典中(这就是传递给视图函数的参数列表)并返回。


题图:pexels,CC0 授权。

点击阅读原文,查看更多 Python 教程和资源。

推荐文章
Python爱好者社区  ·  yyds!论文教程
5 天前
Python爱好者社区  ·  论文入门手册
3 天前
Python爱好者社区  ·  华为校招开了侮辱价。
4 天前
Python爱好者社区  ·  你觉得是上班更苦还是上学更苦?
5 天前
丁香医生  ·  把痰吞进肚子里,要紧吗?
7 年前