一个 web 应用不同的路径会有不同的处理函数,路由就是根据请求的 URL 找到对应处理函数的过程。
在执行查找之前,需要有一个规则列表,它存储了 url 和处理函数的对应关系。最容易想到的解决方案就是定义一个字典,key 是 url,value 是对应的处理函数。如果 url 都是静态的(url 路径都是实现确定的,没有变量和正则匹配),那么路由的过程就是从字典中通过 url 这个 key ,找到并返回对应的 value;如果没有找到,就报 404 错误。而对于动态路由,还需要更复杂的匹配逻辑。flask 中的路由过程是这样的吗?这篇文章就来分析分析。
在分析路由匹配过程之前,我们先来看看 flask 中,构建这个路由规则的两种方法:
1 .通过 @app.route() decorator,比如文章开头给出的 hello world 例子
通过 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 的处理函数,也被称为视图函数
两种方法等价
NOTE: 其实,还有一种方法来构建路由规则——直接操作 app.url_map 这个数据结构。不过这种方法并不是很常用,因此就不展开了。
注册路由规则的时候,flask 内部做了哪些东西呢?我们来看看 route 方法:
def route(self, rule, **options): """A decorator that is used to register a view function for a given URL rule. This does the same thing as :meth:`add_url_rule` but is intended for decorator usage:: @app.route('http://www.likecs.com/') def index(): return 'Hello World‘ """ def decorator(f): endpoint = options.pop('endpoint', None) self.add_url_rule(rule, endpoint, f, **options) return f return decorator通过闭包的形式传递参数给内部函数,route 方法内部也是调用 add_url_rule,只不过在外面包了一层装饰器的逻辑,这也验证了上面两种方法等价的说法。
def add_url_rule(self, rule, endpoint=None, view_func=None, **options): """Connects a URL rule. Works exactly like the :meth:`route` decorator. If a view_func is provided it will be registered with the endpoint. """ methods = options.pop('methods', None) rule = self.url_rule_class(rule, methods=methods, **options) 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) 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 路由逻辑
>>> m = Map([ ... Rule('http://www.likecs.com/', endpoint='index'), ... Rule('/downloads/', endpoint='downloads/index'), ... Rule('/downloads/<int:id>', endpoint='downloads/show') ... ]) >>> urls = m.bind("example.com", "http://www.likecs.com/") >>> urls.match("http://www.likecs.com/", "GET") ('index', {}) >>> urls.match("/downloads/42") ('downloads/show', {'id': 42}) >>> urls.match("/downloads") Traceback (most recent call last): ... RequestRedirect: >>> urls.match("/missing") Traceback (most recent call last): ... 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,继续探寻路由匹配的逻辑: