深入理解Flask路由(2)- werkzeug 路由系统

上一篇我们说到:Flask 的路由机制是在 werkzeug 中实现的, Flask 只是调用而已。Flask 的路由包括三个主要过程:

  • 路由的构建 (werkzeug 实现相关的数据结构,Rule, Map 等)
  • 路由的匹配 (werkzueg 实现)
  • 请求的分派(dispatch, 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

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

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_mapview_functionsFlask.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 下篇再讲,待续。

参考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章