flask 源碼解析:路由

3.flask 源碼解析:路由

構(gòu)建路由規(guī)則

一個(gè) web 應(yīng)用不同的路徑會(huì)有不同的處理函數(shù),路由就是根據(jù)請(qǐng)求的 URL 找到對(duì)應(yīng)處理函數(shù)的過(guò)程。

在執(zhí)行查找之前,需要有一個(gè)規(guī)則列表,它存儲(chǔ)了 url 和處理函數(shù)的對(duì)應(yīng)關(guān)系。最容易想到的解決方案就是定義一個(gè)字典,key 是 url,value 是對(duì)應(yīng)的處理函數(shù)。如果 url 都是靜態(tài)的(url 路徑都是實(shí)現(xiàn)確定的,沒(méi)有變量和正則匹配),那么路由的過(guò)程就是從字典中通過(guò) url 這個(gè) key ,找到并返回對(duì)應(yīng)的 value;如果沒(méi)有找到,就報(bào) 404 錯(cuò)誤。而對(duì)于動(dòng)態(tài)路由,還需要更復(fù)雜的匹配邏輯。flask 中的路由過(guò)程是這樣的嗎?這篇文章就來(lái)分析分析。

在分析路由匹配過(guò)程之前,我們先來(lái)看看 flask 中,構(gòu)建這個(gè)路由規(guī)則的兩種方法:

  1. 通過(guò) [@app.route](http://cizixs.com/cdn-cgi/l/email-protection#bdddfddccdcd93cfd2c8c9d8)() decorator,比如文章開(kāi)頭給出的 hello world 例子

  2. 通過(guò)

app.add_url_rule

,這個(gè)方法的簽名為

add_url_rule(self, rule, endpoint=None, view_func=None, **options)

,參數(shù)的含義如下:

  • rule: url 規(guī)則字符串,可以是靜態(tài)的 /path,也可以包含 /
  • endpoint:要注冊(cè)規(guī)則的 endpoint,默認(rèn)是 view_func 的名字
  • view_func:對(duì)應(yīng) url 的處理函數(shù),也被稱為視圖函數(shù)

這兩種方法是等價(jià)的,也就是說(shuō):

@app.route('/')
def hello():
    return "hello, world!"

也可以寫成

def hello():
    return "hello, world!"

app.add_url_rule('/', 'hello', hello)

NOTE: 其實(shí),還有一種方法來(lái)構(gòu)建路由規(guī)則——直接操作 app.url_map 這個(gè)數(shù)據(jù)結(jié)構(gòu)。不過(guò)這種方法并不是很常用,因此就不展開(kāi)了。

注冊(cè)路由規(guī)則的時(shí)候,flask 內(nèi)部做了哪些東西呢?我們來(lái)看看 route 方法:

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 方法內(nèi)部也是調(diào)用 add_url_rule,只不過(guò)在外面包了一層裝飾器的邏輯,這也驗(yàn)證了上面兩種方法等價(jià)的說(shuō)法。

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 和構(gòu)建 methods 的部分邏輯,可以看到它主要做的事情就是更新 self.url_mapself.view_functions 兩個(gè)變量。找到變量的定義,發(fā)現(xiàn) url_mapwerkzeug.routeing:Map 類的對(duì)象,rulewerkzeug.routing:Rule 類的對(duì)象,view_functions 就是一個(gè)字典。這和我們之前預(yù)想的并不一樣,這里增加了 RuleMap 的封裝,還把 urlview_func 保存到了不同的地方。

需要注意的是:每個(gè)視圖函數(shù)的 endpoint 必須是不同的,否則會(huì)報(bào) AssertionError。

werkzeug 路由邏輯

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

>>> m = Map([
...     Rule('/', endpoint='index'),
...     Rule('/downloads/', endpoint='downloads/index'),
...     Rule('/downloads/<int:id>', 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 最核心的路由功能:添加路由規(guī)則(也可以使用 m.add),把路由表綁定到特定的環(huán)境(m.bind),匹配url(urls.match)。正常情況下返回對(duì)應(yīng)的 endpoint 名字和參數(shù)字典,可能報(bào)重定向或者 404 異常。

可以發(fā)現(xiàn),endpoint 在路由過(guò)程中非常重要werkzeug 的路由過(guò)程,其實(shí)是 url 到 endpoint 的轉(zhuǎn)換:通過(guò) url 找到處理該 url 的 endpoint。至于 endpoint 和 view function 之間的匹配關(guān)系,werkzeug 是不管的,而上面也看到 flask 是把這個(gè)存放到字典中的。

flask 路由實(shí)現(xiàn)

好,有了這些基礎(chǔ)知識(shí),我們回頭看 dispatch_request,繼續(xù)探尋路由匹配的邏輯:

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)

這個(gè)方法做的事情就是找到請(qǐng)求對(duì)象 request,獲取它的 endpoint,然后從 view_functions 找到對(duì)應(yīng) endpointview_func ,把請(qǐng)求參數(shù)傳遞過(guò)去,進(jìn)行處理并返回。view_functions 中的內(nèi)容,我們已經(jīng)看到,是在構(gòu)建路由規(guī)則的時(shí)候保存進(jìn)去的;那請(qǐng)求中 req.url_rule 是什么保存進(jìn)去的呢?它的格式又是什么?

我們可以先這樣理解:_request_ctx_stack.top.request 保存著當(dāng)前請(qǐng)求的信息,在每次請(qǐng)求過(guò)來(lái)的時(shí)候,flask 會(huì)把當(dāng)前請(qǐng)求的信息保存進(jìn)去,這樣我們就能在整個(gè)請(qǐng)求處理過(guò)程中使用它。至于怎么做到并發(fā)情況下信息不會(huì)相互干擾錯(cuò)亂,我們將在下一篇文章介紹。

_request_ctx_stack 中保存的是 RequestContext 對(duì)象,它出現(xiàn)在 flask/ctx.py 文件中,和路由相關(guān)的邏輯如下:

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'])

在初始化的時(shí)候,會(huì)調(diào)用 app.create_url_adapter 方法,把 appurl_map 綁定到 WSGI environ 變量上(bind_to_environ 和之前的 bind 方法作用相同)。最后會(huì)調(diào)用 match_request 方法,這個(gè)方式調(diào)用了 url_adapter.match 方法,進(jìn)行實(shí)際的匹配工作,返回匹配的 url rule。而我們之前使用的 url_rule.endpoint 就是匹配的 endpoint 值。

整個(gè) flask 的路由過(guò)程就結(jié)束了,總結(jié)一下大致的流程:

  • 通過(guò) [@app.route](http://cizixs.com/cdn-cgi/l/email-protection#c3a383a2b3b3edb1acb6b7a6)或者app.add_url_rule 注冊(cè)應(yīng)用 url 對(duì)應(yīng)的處理函數(shù)
  • 每次請(qǐng)求過(guò)來(lái)的時(shí)候,會(huì)事先調(diào)用路由匹配的邏輯,把路由結(jié)果保存起來(lái)
  • dispatch_request 根據(jù)保存的路由結(jié)果,調(diào)用對(duì)應(yīng)的視圖函數(shù)

match 實(shí)現(xiàn)

雖然講完了 flask 的路由流程,但是還沒(méi)有講到最核心的問(wèn)題:werkzeug 中是怎么實(shí)現(xiàn) match 方法的。Map 保存了 Rule 列表,match 的時(shí)候會(huì)依次調(diào)用其中的 rule.match 方法,如果匹配就找到了 match。Rule.match 方法的代碼如下:

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

它的邏輯是這樣的:用實(shí)現(xiàn) compile 的正則表達(dá)式去匹配給出的真實(shí)路徑信息,把所有的匹配組件轉(zhuǎn)換成對(duì)應(yīng)的值,保存在字典中(這就是傳遞給視圖函數(shù)的參數(shù)列表)并返回。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容