深入理解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 下篇再講,待續。

參考

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