上一篇我們說到: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_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 下篇再講,待續。