文章屬於作者原創,原文釋出在個人部落格。
這是 flask 原始碼解析系列文章的其中一篇,本系列所有文章列表:
構建路由規則
一個 web 應用不同的路徑會有不同的處理函式,路由就是根據請求的 URL 找到對應處理函式的過程。
在執行查詢之前,需要有一個規則列表,它儲存了 url 和處理函式的對應關係。最容易想到的解決方案就是定義一個字典,key 是 url,value 是對應的處理函式。如果 url 都是靜態的(url 路徑都是實現確定的,沒有變數和正則匹配),那麼路由的過程就是從字典中通過 url 這個 key ,找到並返回對應的 value;如果沒有找到,就報 404 錯誤。而對於動態路由,還需要更復雜的匹配邏輯。flask 中的路由過程是這樣的嗎?這篇文章就來分析分析。
在分析路由匹配過程之前,我們先來看看 flask
中,構建這個路由規則的兩種方法:
- 通過
@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 的處理函式,也被稱為檢視函式
這兩種方法是等價的,也就是說:
1 2 3 |
@app.route('/') def hello(): return "hello, world!" |
也可以寫成
1 2 3 4 |
def hello(): return "hello, world!" app.add_url_rule('/', 'hello', hello) |
NOTE: 其實,還有一種方法來構建路由規則——直接操作 app.url_map
這個資料結構。不過這種方法並不是很常用,因此就不展開了。
註冊路由規則的時候,flask 內部做了哪些東西呢?我們來看看 route
方法:
1 2 3 4 5 6 7 8 9 10 11 12 |
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. """ def decorator(f): endpoint = options.pop('endpoint', None) self.add_url_rule(rule, endpoint, f, **options) return f return decorator |
route
方法內部也是呼叫 add_url_rule
,只不過在外面包了一層裝飾器的邏輯,這也驗證了上面兩種方法等價的說法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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 路由邏輯
事實上,flask 核心的路由邏輯是在 werkzeug
中實現的。所以在繼續分析之前,我們先看一下 werkzeug
提供的路由功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
>>> m = Map([ ... Rule('/', endpoint='index'), ... Rule('/downloads/', endpoint='downloads/index'), ... Rule('/downloads/', endpoint='downloads/show') ... ]) >>> urls = m.bind("example.com", "/") >>> urls.match("/", "GET") ('index', {}) >>> urls.match("/downloads/42") ('downloads/show', {'id': 42}) >>> urls.match("/downloads") Traceback (most recent call last): ... RequestRedirect: http://example.com/downloads/ >>> 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
,繼續探尋路由匹配的邏輯:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def dispatch_request(self): """Does the request dispatching. Matches the URL and returns the return value of the view or error handler. This does not have to be a response object. In order to convert the return value to a proper response object, call :func:`make_response`. """ req = _request_ctx_stack.top.request if req.routing_exception is not None: self.raise_routing_exception(req) rule = req.url_rule # dispatch to the handler for that endpoint return self.view_functions[rule.endpoint](**req.view_args) |
這個方法做的事情就是找到請求物件 request
,獲取它的 endpoint
,然後從 view_functions
找到對應 endpoint
的 view_func
,把請求引數傳遞過去,進行處理並返回。view_functions
中的內容,我們已經看到,是在構建路由規則的時候儲存進去的;那請求中 req.url_rule
是什麼儲存進去的呢?它的格式又是什麼?
我們可以先這樣理解:_request_ctx_stack.top.request
儲存著當前請求的資訊,在每次請求過來的時候,flask
會把當前請求的資訊儲存進去,這樣我們就能在整個請求處理過程中使用它。至於怎麼做到併發情況下資訊不會相互干擾錯亂,我們將在下一篇文章介紹。
_request_ctx_stack
中儲存的是 RequestContext
物件,它出現在 flask/globals.py
檔案中,和路由相關的邏輯如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
class RequestContext(object): def __init__(self, app, environ, request=None): self.app = app self.request = request self.url_adapter = app.create_url_adapter(self.request) self.match_request() def match_request(self): """Can be overridden by a subclass to hook into the matching of the request. """ try: url_rule, self.request.view_args = self.url_adapter.match(return_rule=True) self.request.url_rule = url_rule except HTTPException as e: self.request.routing_exception = e class Flask(_PackageBoundObject): def create_url_adapter(self, request): """Creates a URL adapter for the given request. The URL adapter is created at a point where the request context is not yet set up so the request is passed explicitly. """ if request is not None: return self.url_map.bind_to_environ(request.environ, server_name=self.config['SERVER_NAME']) |
在初始化的時候,會呼叫 app.create_url_adapter
方法,把 app
的 url_map
繫結到 WSGI environ 變數上(bind_to_environ
和之前的 bind
方法作用相同)。最後會呼叫 match_request
方法,這個方式呼叫了 url_adapter.match
方法,進行實際的匹配工作,返回匹配的 url rule。而我們之前使用的 url_rule.endpoint
就是匹配的 endpoint 值。
整個 flask
的路由過程就結束了,總結一下大致的流程:
- 通過
@app.route
或者app.add_url_rule
註冊應用 url 對應的處理函式 - 每次請求過來的時候,會事先呼叫路由匹配的邏輯,把路由結果儲存起來
dispatch_request
根據儲存的路由結果,呼叫對應的檢視函式
match 實現
雖然講完了 flask
的路由流程,但是還沒有講到最核心的問題:werkzeug
中是怎麼實現 match
方法的。Map
儲存了 Rule
列表,match
的時候會依次呼叫其中的 rule.match
方法,如果匹配就找到了 match。Rule.match
方法的程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
def match(self, path): """Check if the rule matches a given path. Path is a string in the form ``"subdomain|/path(method)"`` and is assembled by the map. If the map is doing host matching the subdomain part will be the host instead. If the rule matches a dict with the converted values is returned, otherwise the return value is `None`. """ if not self.build_only: m = self._regex.search(path) if m is not None: groups = m.groupdict() result = {} for name, value in iteritems(groups): try: value = self._converters[name].to_python(value) except ValidationError: return result[str(name)] = value if self.defaults: result.update(self.defaults) return result |
它的邏輯是這樣的:用實現 compile 的正規表示式去匹配給出的真實路徑資訊,把所有的匹配元件轉換成對應的值,儲存在字典中(這就是傳遞給檢視函式的引數列表)並返回。