本系列共三篇文章:
深入理解Flask路由的实现机制
深入理解Flask路由(2)- werkzeug 路由系统
深入理解Flask路由 (3) - 动态 url 及转换器
上一篇我们说到:Flask 的路由机制是在 werkzeug 中实现的, Flask 只是调用而已。Flask 的路由包括三个主要过程:
本篇先介绍 werkzeug 两个重要的数据结构 Rule 和 Map,后续再把三个阶段串起来。
Flask 的路由主要是根据前端请求 (request) 中 url path 信息,找到对应的 endpoint,再根据 endpoint 找到对应的 view function 进行处理。在这个关系中,url 与 endpoint 的映射,由 werkzeug 的 Rule 和 Map 来实现,endpoint 与 view function 的映射由 Flask 用 Python 标准数据结构 dict 实现。 在 werkzeug 中,Rule 表示 url rule 和 endpoint 的映射,Map 实现与多个 Rule 的绑定。看下面的一段代码:
from werkzeug.routing import Map, Rule
url_rules = [
Rule('/', endpoint='index', methods=['GET']),
Rule('/about', endpoint='about', methods=['GET'])
]
map = Map(url_rules)
print(map)
在这段代码中,我们定义了两个 rule rule,然后与 Map 绑定,运行代码,打印出下面的结果:
Map([<Rule '/about' (HEAD, GET) -> about>,
<Rule '/' (HEAD, GET) -> index>])
Rule 定义 url rule 和 endpoint 之间的映射,url rule 与 endpoint 是多对一关系。通过 Rule 的初始化方法 __init__()
可以知道如何创建一个 Rule 实例。下面介绍主要知识点,而不说明每一个细节。
def __init__(
self,
string,
defaults=None,
subdomain=None,
methods=None,
build_only=False,
endpoint=None,
strict_slashes=None,
redirect_to=None,
alias=False,
host=None,
):
# 代码略
Rule 的第一个参数 string (str 类型),表示 url 的路径规则 (url rule),比如刚才的例子中 ‘/’ 和 ‘/about’。rule 的标准格式是 <converter(arguments):name>
,由转换器 (converter)、转换器参数(argument) 和名称(name)构成。converter 在路由匹配的时候会用到,这里暂且不表。converter 可以省略,默认值为 UnicodeConverter
。
Url rule 必须从 /
开始。/
的英文为 slash,了解其英文有助于看懂代码。如果不以 slash 开始,抛出如下错误:
# FILE: werkzeug/routing.py
if not string.startswith("/"):
raise ValueError("urls must start with a leading slash")
url path 的结尾,有以 /
结尾和不以 /
结尾两种可能 。比如有些人用 /about
表示,有些用 /about/
表示(多了一个斜杠)。其实这两个应该是同一个 url。为了保证 url 的唯一性,避免被搜索引擎索引两次,Rule 和 Map 提供了相关属性对此进行区分:
is_leaf
属性:如果 url 以 /
结束,则表示这个 url 是 branch url (枝),否则就是 leaf url (叶), is_leaf = True
。Rule 还有另外一个 strict_slash
属性(Map 也有 strict_slash
属性,默认为 True)。如果 strict_slash
为 True, 则 url 不以 /
结尾时重定向到以 /
结尾的 url。我们用一段代码来说明 strict slash 和重定向的关系:
from flask import Flask
from werkzeug.routing import Map, Rule
app = Flask(__name__)
@app.route('/')
def index():
return 'Index Page'
@app.route('/about/')
def about():
return 'About Page'
if __name__ == '__main__':
app.run()
在这种情况下,如果访问 /about
会重定向到 /about/
。因为 strict_slash
默认为 True。
127.0.0.1 - - [30/Mar/2020 22:19:45] "GET /about HTTP/1.1" 308 -
127.0.0.1 - - [30/Mar/2020 22:19:45] "GET /about/ HTTP/1.1" 200 -
如果将装饰器改为 /about
:
@app.route('/about')
def about():
return 'About Page'
此时访问 /about
成功,而访问 /about/
则出现 404 Not Found 错误:
127.0.0.1 - - [30/Mar/2020 22:21:34] "GET /about HTTP/1.1" 200 -
127.0.0.1 - - [30/Mar/2020 22:21:40] "GET /about/ HTTP/1.1" 404 -
如果我们将 app.url_map.strict_slash
改为 False, 则 /about
和 /about/
能分别被访问。不建议这样用,因为违背了唯一 url 原则。
app = Flask(__name__)
app.url_map.strict_slashes = False # 添加一句代码,默认值为True
127.0.0.1 - - [30/Mar/2020 22:24:19] "GET /about HTTP/1.1" 200 -
127.0.0.1 - - [30/Mar/2020 22:24:24] "GET /about/ HTTP/1.1" 200 -
methods
指 rule 适用的 HTTP method,比如 GET, POST 等。如果不指定 method,则所有的 method 都被允许。如果方法中有 GET ,则 HEAD 方法被自动添加。methods 参数可以是 list, tupple 或 set 这样的集合,通常是 list。看看 Rule.__init__()
方法的代码,理解这些要点:
# FILE: werkzeug/routing.py
# Rule.__init__() method:
if methods is not None:
if isinstance(methods, str):
raise TypeError("'methods' should be a list of strings.")
methods = {x.upper() for x in methods}
if "HEAD" not in methods and "GET" in methods:
methods.add("HEAD")
if websocket and methods - {"GET", "HEAD", "OPTIONS"}:
raise ValueError(
"WebSocket rules can only use 'GET', 'HEAD', and 'OPTIONS' methods."
)
self.methods = methods
Map 绑定多个 Rule,在本篇开始的示例代码中,我们是这样创建 Map 实例的:
map = Map(url_rules)
之所以可以这样构造,是因为 Map.__init__()
方法自动调用了下面一系列方法:
- Map.add() : 封装方法,调用 Rule.bind()
- Rule.bind() : 核心代码,提供 Map 与 Rule 绑定的实现
下面列出主要的代码:
# Map.__init__()
for rulefactory in rules or ():
self.add(rulefactory)
下面是 Map.add()
方法的代码:
def add(self, rulefactory):
for rule in rulefactory.get_rules(self):
rule.bind(self)
self._rules.append(rule)
self._rules_by_endpoint.setdefault(rule.endpoint, []).append(rule)
self._remap = True
继续看 rule.bind()
的代码。bind()
方法执行核心的操作,将 rule 绑定到 Map, 并且基于 rule 创建正则表达式,保存在 Map._rules
属性中。
def bind(self, map, rebind=False):
"""Bind the url to a map and create a regular expression based on
the information from the rule itself and the defaults from the map.
:internal:
"""
if self.map is not None and not rebind:
raise RuntimeError("url rule %r already bound to map %r" % (self, self.map))
self.map = map
if self.strict_slashes is None:
self.strict_slashes = map.strict_slashes
if self.subdomain is None:
self.subdomain = map.default_subdomain
self.compile()
了解了 Rule 和 Map 的实现细节,我们接下来不用装饰器和 add_url_rule()
方法来实现 Flask 路由,以加深对相关知识点的理解。
首先,简单来说,Flask 路由信息包括 url_map
和 view_functions
。Flask.url_map
属性是 Map 的实例, 我们将 Map 传给它就可以了。view_functions
是 dict,可以直接创建。示例代码如下:
from flask import Flask
from werkzeug.routing import Map, Rule
app = Flask(__name__)
def index():
return 'Index Page'
def about():
return 'About Page'
# create url rules and map instances
url_rules = [
Rule('/', endpoint='index', methods=['GET']),
Rule('/about', endpoint='about', methods=['GET'])
]
map = Map(url_rules)
# set app.url_map and view_functions
app.url_map = map
app.view_functions = {
'index': index,
'about': about
}
if __name__ == '__main__':
app.run()
但这段代码有一个小问题。我们知道 Flask 为了能处理静态文件,在.__init__()
方法中构造了一个 static rule,上面的代码直接对 url_map
赋值,初始化方法中创建的 static rule 就被丢掉了。为了保留 static rule,可以模拟 Map 添加 Rule 的方式,稍作变更,以下是代码,我略去了重复的部分。
url_rules = [
Rule('/', endpoint='index', methods=['GET']),
Rule('/about', endpoint='about', methods=['GET'])
]
for rule in url_rules:
app.url_map.add(rule)
app.view_functions['index'] = index
app.view_functions['about'] = about
一般情况下,我们并不需要用这种方式来编写 Flask 路由代码,本示例仅仅为了理解机制特意为之,回过头来,我们再去看看 Flask.add_url_rule()
方法,可以看到 Flask 也是这么做的:
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
# 其他代码略
rule = self.url_rule_class(rule, methods=methods, **options)
self.url_map.add(rule)
# 添加view functions
if view_func is not None:
self.view_functions[endpoint] = view_func
Map 另外一个核心知识点 converter 下篇再讲,待续。