flask 原始碼解析:路由

發表於2017-02-08

文章屬於作者原創,原文釋出在個人部落格

這是 flask 原始碼解析系列文章的其中一篇,本系列所有文章列表:

構建路由規則

一個 web 應用不同的路徑會有不同的處理函式,路由就是根據請求的 URL 找到對應處理函式的過程。

在執行查詢之前,需要有一個規則列表,它儲存了 url 和處理函式的對應關係。最容易想到的解決方案就是定義一個字典,key 是 url,value 是對應的處理函式。如果 url 都是靜態的(url 路徑都是實現確定的,沒有變數和正則匹配),那麼路由的過程就是從字典中通過 url 這個 key ,找到並返回對應的 value;如果沒有找到,就報 404 錯誤。而對於動態路由,還需要更復雜的匹配邏輯。flask 中的路由過程是這樣的嗎?這篇文章就來分析分析。

在分析路由匹配過程之前,我們先來看看 flask 中,構建這個路由規則的兩種方法:

  1. 通過 @app.route() decorator,比如文章開頭給出的 hello world 例子
  2. 通過 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 的處理函式,也被稱為檢視函式

這兩種方法是等價的,也就是說:

也可以寫成

NOTE: 其實,還有一種方法來構建路由規則——直接操作 app.url_map 這個資料結構。不過這種方法並不是很常用,因此就不展開了。

註冊路由規則的時候,flask 內部做了哪些東西呢?我們來看看 route 方法:

route 方法內部也是呼叫 add_url_rule,只不過在外面包了一層裝飾器的邏輯,這也驗證了上面兩種方法等價的說法。

上面這段程式碼省略了處理 endpoint 和構建 methods 的部分邏輯,可以看到它主要做的事情就是更新 self.url_mapself.view_functions 兩個變數。找到變數的定義,發現 url_mapwerkzeug.routeing:Map 類的物件,rulewerkzeug.routing:Rule 類的物件,view_functions 就是一個字典。這和我們之前預想的並不一樣,這裡增加了 RuleMap 的封裝,還把 urlview_func 儲存到了不同的地方。

需要注意的是:每個檢視函式的 endpoint 必須是不同的,否則會報 AssertionError

werkzeug 路由邏輯

事實上,flask 核心的路由邏輯是在 werkzeug 中實現的。所以在繼續分析之前,我們先看一下 werkzeug 提供的路由功能

上面的程式碼演示了 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,繼續探尋路由匹配的邏輯:

這個方法做的事情就是找到請求物件 request,獲取它的 endpoint,然後從 view_functions 找到對應 endpointview_func ,把請求引數傳遞過去,進行處理並返回。view_functions 中的內容,我們已經看到,是在構建路由規則的時候儲存進去的;那請求中 req.url_rule 是什麼儲存進去的呢?它的格式又是什麼?

我們可以先這樣理解:_request_ctx_stack.top.request 儲存著當前請求的資訊,在每次請求過來的時候,flask 會把當前請求的資訊儲存進去,這樣我們就能在整個請求處理過程中使用它。至於怎麼做到併發情況下資訊不會相互干擾錯亂,我們將在下一篇文章介紹。

_request_ctx_stack 中儲存的是 RequestContext 物件,它出現在 flask/globals.py 檔案中,和路由相關的邏輯如下:

在初始化的時候,會呼叫 app.create_url_adapter 方法,把 appurl_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 方法的程式碼如下:

它的邏輯是這樣的:用實現 compile 的正規表示式去匹配給出的真實路徑資訊,把所有的匹配元件轉換成對應的值,儲存在字典中(這就是傳遞給檢視函式的引數列表)並返回。

相關文章