flask/odoo/werkzeug的url mapping

參考:
Werkzeug庫——routing模塊
flask 源碼解析:路由
odoo(8.0)源碼
werkzeug(0.14.1)源碼
flask(0.11.1)源碼

一個web框架必須解決一個問題:當一個Request進入系統(tǒng)時,怎樣去確定使用哪個函數(shù)或方法來處理。

Django自己處理這個問題。
Flask和Odoo(一個OpenERP)使用Werkzeug庫(本身就是Flask的關(guān)聯(lián)庫)。

Werkzeug定義了三個類:
werkzeug.routing.Map
werkzeug.routing.MapAdapter
werkzeug.routing.Rule

Map的實例map存儲所有的URL規(guī)則,這些規(guī)則就是Rule的實例rule

一、Map

add(self, rulefactory)
該方法會將傳入的rule,通過rulebind方法來與map實例關(guān)聯(lián)。并且,在map_rules屬性中插入rule實例,在_rules_by_endpoint屬性中,創(chuàng)建rule.endpointrule實例的關(guān)聯(lián)。
具體代碼如下:

    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

_rules_by_endpoint可見,一個endpoint可對應(yīng)多個rule。

bind(self, server_name, ..., path_info, ...)
返回一個MapAdapter實例map_adapter。

bind_to_environ(self, environ, server_name=None, subdomain=None)
調(diào)用上述的bind方法,傳入environ中的信息。比如說path_info,request_method等等。

二、MapAdapter

該類執(zhí)行具體的URL匹配工作。

__init__(self, map, server_name, script_name, subdomain, url_scheme, path_info, default_method, query_args=None)
初始化時,會處理傳入的map

    def __init__(self, map, ...):
        self.map = map

match(self, path_info=None, method=None, return_rule=False, query_args=None)
通過傳入的path_info(路徑信息,若為None,則使用初始化時傳入的path_info),和method(HTTP方法)來從self.map._rules中找到匹配的rule(通過調(diào)用rule.match(path, method)),從而返回ruleendpoint和一些參數(shù)rv。

dispatch(self, view_func, path_info=None, method=None, catch_http_exceptions=False)
調(diào)用match方法,如果找到了對應(yīng)的rule,則會執(zhí)行該rule對應(yīng)的view_func(視圖函數(shù))。

三、Rule

繼承自RuleFactory

__init__(self, string, defaults=None, subdomain=None, methods=None, build_only=False, endpoint=None, strict_slashes=None, redirect_to=None, alias=False, host=None)
string就是url,另兩個關(guān)鍵關(guān)鍵參數(shù)是endpointmethods。

get_rules(self, map)
返回本身。

bind(self, map, rebind=False)
將自身與map綁定。
調(diào)用compile方法,依據(jù)rulemap,創(chuàng)建一個正則表達式。這其實就是綁定的實質(zhì)。

compile(self)
依據(jù)rulemap二者的信息,創(chuàng)建一個正則表達式,用于后續(xù)匹配。

match(self, path, method=None)
進行匹配。

四、Endpoint

Werkzeug本身不定義Endpoint。這個類主要的作用是將Rule與最終用于處理的視圖函數(shù)進行關(guān)聯(lián)。從上述內(nèi)容可知,順序應(yīng)該是:urlruleendpointview_func。但最后一步具體怎么做,Werkzeug是不管的。

五、整體流程

構(gòu)建階段:

  • 創(chuàng)建Map實例map。
  • 不論是在map初始化時,還是直接調(diào)用map.add,將mapRule實例rule關(guān)聯(lián)。
    • rule初始化時需要傳入urlendpoint
    • map.add方法中,rule會調(diào)用bind方法,與map綁定。
    • rule.bind的方法中,會調(diào)用compile方法,生成一個正則表達式,用于后續(xù)的匹配。

匹配階段:

  • map使用方法bind_to_environenviron關(guān)聯(lián)。
  • 方法bind_to_environ調(diào)用bind方法,返回一個MapAdapter實例map_adapter。
  • 調(diào)用map_adaptermatch方法,判斷是否有與path_info(從environ中獲?。?yīng)的rule,有則返回rule.endpoint。
  • 通過endpoint,找到對應(yīng)的view_func。

六,F(xiàn)lask的路由

flask.app中的Flask中。
構(gòu)建示例:

from flask import Flask
app = Flask(__name__)

@app.route('/', methods=['GET'])
def index():
    return '<h1>Hello World</h1>', 200

構(gòu)建邏輯:

    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::

            @app.route('/')
            def index():
                return 'Hello World'

        For more information refer to :ref:`url-route-registrations`.

        :param rule: the URL rule as string
        :param endpoint: the endpoint for the registered URL rule.  Flask
                         itself assumes the name of the view function as
                         endpoint
        :param options: the options to be forwarded to the underlying
                        :class:`~werkzeug.routing.Rule` object.  A change
                        to Werkzeug is handling of method options.  methods
                        is a list of methods this rule should be limited
                        to (``GET``, ``POST`` etc.).  By default a rule
                        just listens for ``GET`` (and implicitly ``HEAD``).
                        Starting with Flask 0.6, ``OPTIONS`` is implicitly
                        added and handled by the standard request handling.
        """
        def decorator(f):
            endpoint = options.pop('endpoint', None)
            self.add_url_rule(rule, endpoint, f, **options)
            return f
        return decorator

實質(zhì)上是調(diào)用add_url_rule方法,也可直接調(diào)用。
等價于:

def index():
    return "<h1>Hello, World</h1>", 200

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

該方法的入?yún)?em>rule(其實就是url),endpoint,f(視圖函數(shù))。
在Flask中,endpoint默認定義為fname。
從幫助文檔可以看出,options其實是為了Rule。

add_url_rule方法:

    def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
        if endpoint is None:
            endpoint = _endpoint_from_view_func(view_func)
        options['endpoint'] = 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

首先創(chuàng)建Rule的實例rule
然后加入到Map的實例self.url_map中,ruleurl_map進行了綁定。
Flask中endpointview_func的對應(yīng)關(guān)系通過一個字典view_functions來保存。它們倆是一一對應(yīng)的。

匹配邏輯dispatch_request方法:

    def dispatch_request(self):
        req = _request_ctx_stack.top.request
        if req.routing_exception is not None:
            self.raise_routing_exception(req)
        rule = req.url_rule

        return self.view_functions[rule.endpoint](**req.view_args)

首先通過req找到rule,然后直接在字典view_functions通過鍵rule.endpoint就可以找到對應(yīng)的視圖函數(shù)了。
關(guān)鍵是req是怎么來的。
_request_ctx_stack中保存RequestContext對象。

class RequestContext(object):
    def __init__(self, app, environ, request=None):
        self.app = app
        if request is None:
            request = app.request_class(environ)
        self.request = request
        self.url_adapter = app.create_url_adapter(self.request)

        self.match_request()

    def match_request(self):
        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):
        if request is not None:
            return self.url_map.bind_to_environ(request.environ,
                server_name=self.config['SERVER_NAME'])

        if self.config['SERVER_NAME'] is not None:
            return self.url_map.bind(
                self.config['SERVER_NAME'],
                script_name=self.config['APPLICATION_ROOT'] or '/',
                url_scheme=self.config['PREFERRED_URL_SCHEME'])

app.create_url_adapter通過url_mapbind方法,來返回一個MapAdapter實例,設(shè)置為RequestContexturl_adapter屬性。
接著調(diào)用match_request方法,本質(zhì)就是調(diào)用url_adaptermatch方法,找到對應(yīng)的rule來匹配environ中的path_info
由于match方法設(shè)置了return_rule=True,所以返回的不是endpoint而是rule
這樣req.url_rule就設(shè)置好了。

七,Odoo的路由

odoo.openerp.http中。
構(gòu)建階段的主邏輯如下:

def routing_map(modules, nodb_only, converters=None):
    routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)

    for module in modules:
        for _, cls in controllers_per_module[module]:
            o = cls()
            members = inspect.getmembers(o, inspect.ismethod)
            for _, mv in members:
                if hasattr(mv, 'routing'):
                    routing = dict(type='http', auth='user', methods=None, routes=None)
                    methods_done = list()
                    if not nodb_only or routing['auth'] == "none":
                        endpoint = EndPoint(mv, routing)
                        for url in routing['routes']:
                            if routing.get("combine", False):
                                url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
                                if url.endswith("/") and len(url) > 1:
                                    url = url[: -1]

                            xtra_keys = 'defaults subdomain build_only strict_slashes redirect_to alias host'.split()
                            kw = {k: routing[k] for k in xtra_keys if k in routing}
                            routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods'], **kw))
    return routing_map

Odoo只會調(diào)用這個函數(shù)一次。
先創(chuàng)建map實例。
然后遍歷Odoo中的所有module,找到所有的routefunc關(guān)系,為它們創(chuàng)建rule實例,加到map中。
具體而言,找到類型為controller的類cls,再找到cls的方法。
若某一方法mvrouting屬性,則該方法確定是被裝飾器工廠函數(shù)route裝飾的視圖方法。而所謂的routing屬性,是一個字典,內(nèi)容是該裝飾器工廠函數(shù)的關(guān)鍵字參數(shù),另加別的一些內(nèi)容。
字典routing的鍵routes對應(yīng)的值是一個列表,里面存放urls,也就是說,一個視圖方法func可以對應(yīng)多個url。但endpoint是和mv一一映射的,所以所有的rule都是使用同一個endpoint。
這樣,rule初始化的參數(shù)就都有了!
關(guān)于endpoint,Odoo中是這樣定義的:

class EndPoint(object):
    def __init__(self, method, routing):
        self.method = method
        self.original = getattr(method, 'original_func', method)
        self.routing = routing
        self.arguments = {}

    def __call__(self, *args, **kw):
        return self.method(*args, **kw)

可見endpoint是一個可調(diào)用類,執(zhí)行時本質(zhì)上是調(diào)用視圖函數(shù)mv,也就是說,只是視圖函數(shù)的一個簡單包裝而已。

調(diào)用階段的主邏輯如下:

class Root(object):
    """Root WSGI application for the OpenERP Web Client.
    """

    @lazy_property
    def nodb_routing_map(self):
        return routing_map([''] + openerp.conf.server_wide_modules, True)

    def __call__(self, environ, start_response):
        """ Handle a WSGI request
        """
        if not self._loaded:
            self._loaded = True
            self.load_addons()
        return self.dispatch(environ, start_response)

    def dispatch(self, environ, start_response):
        """
        Performs the actual WSGI dispatching for the application.
        """
        try:
            httprequest = werkzeug.wrappers.Request(environ)
            request = self.get_request(httprequest)

            def _dispatch_nodb():
                try:
                    func, arguments = self.nodb_routing_map.bind_to_environ(request.httprequest.environ).match()
                except werkzeug.exceptions.HTTPException, e:
                    return request._handle_exception(e)
                request.set_handler(func, arguments, "none")
                result = request.dispatch()
                return result

            with request:
                result = _dispatch_nodb()
                response = self.get_response(httprequest, result, explicit_session)
            return response(environ, start_response)

        except werkzeug.exceptions.HTTPException, e:
            return e(environ, start_response)

Root的實例是可調(diào)用對象,就是WSGI協(xié)議中的application
路由功能主要是以下這一行:
func, arguments = self.nodb_routing_map.bind_to_environ(request.httprequest.environ).match()
其中self.nodb_routing_map就是一個map實例,bind_to_environ方法返回一個map_adapter實例,match方法返回endpoint和一些參數(shù)。
具體的執(zhí)行視圖函數(shù)語句在request.dispatch()方法中:result = self._call_function(self.params)。

八、一點小比較

Flask中視圖函數(shù)一旦使用裝飾器,那么立馬就會創(chuàng)建ruleapp.rule_map進行綁定,比較靈活。而Odoo就比較挫,要統(tǒng)一進行遍歷。
但是Flask的url處理就比較簡單,一個view_func只能對應(yīng)一個url,這點就不如Odoo。
Flask有Blueprint可以靈活處理視圖函數(shù),所謂的app.register_blueprint本質(zhì)上還是調(diào)用appadd_url_rule方法。Odoo由于限制較多,沒這個場景。


最后的吐槽:搞了半天,還是正則匹配。

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

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

  • 本文大致梳理一下Flask框架在處理url路由時的主要過程。 類圖 route裝飾器 在Flask應(yīng)用中,我們一般...
    Jakiro閱讀 3,738評論 0 6
  • flask源碼分析 1. 前言 本文將基于flask 0.1版本(git checkout 8605cc3)來分析...
    甘尼克斯_閱讀 2,886評論 1 0
  • 3.flask 源碼解析:路由 構(gòu)建路由規(guī)則 一個 web 應(yīng)用不同的路徑會有不同的處理函數(shù),路由就是根據(jù)請求的 ...
    火雞不肥閱讀 616評論 0 0
  • 通常有3種定義路由函數(shù)的方法: 使用flask.Flask.route() 修飾器。 使用flask.Flask....
    黃智勇atTAFinder閱讀 6,339評論 0 5
  • 一聲猿嘯山河遠 萬里狂歌日月長 獨棹江湖風浪險 飛刀做槳探花郎 一聲猿嘯山河遠 萬里狂歌日月長 獨棹江湖風浪險 飛...
    愛詩的呆公子閱讀 4,158評論 62 161

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