flask/odoo/werkzeug的url mapping

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

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

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

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

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

一、Map

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

    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可對應多個rule

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

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

二、MapAdapter

該類執行具體的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(通過調用rule.match(path, method)),從而返回ruleendpoint和一些參數rv

dispatch(self, view_func, path_info=None, method=None, catch_http_exceptions=False)
調用match方法,如果找到了對應的rule,則會執行該rule對應的view_func(視圖函數)。

三、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,另兩個關鍵關鍵參數是endpointmethods

get_rules(self, map)
返回本身。

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

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

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

四、Endpoint

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

五、整體流程

構建階段:

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

匹配階段:

  • map使用方法bind_to_environenviron關聯。
  • 方法bind_to_environ調用bind方法,返回一個MapAdapter實例map_adapter
  • 調用map_adaptermatch方法,判斷是否有與path_info(從environ中獲取)對應的rule,有則返回rule.endpoint
  • 通過endpoint,找到對應的view_func

六,Flask的路由

flask.app中的Flask中。
構建示例:

from flask import Flask
app = Flask(__name__)

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

構建邏輯:

    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

實質上是調用add_url_rule方法,也可直接調用。
等價于:

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

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

該方法的入參包括rule(其實就是url),endpointf(視圖函數)。
在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

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

匹配邏輯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就可以找到對應的視圖函數了。
關鍵是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實例,設置為RequestContexturl_adapter屬性。
接著調用match_request方法,本質就是調用url_adaptermatch方法,找到對應的rule來匹配environ中的path_info
由于match方法設置了return_rule=True,所以返回的不是endpoint而是rule
這樣req.url_rule就設置好了。

七,Odoo的路由

odoo.openerp.http中。
構建階段的主邏輯如下:

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只會調用這個函數一次。
先創建map實例。
然后遍歷Odoo中的所有module,找到所有的routefunc關系,為它們創建rule實例,加到map中。
具體而言,找到類型為controller的類cls,再找到cls的方法。
若某一方法mvrouting屬性,則該方法確定是被裝飾器工廠函數route裝飾的視圖方法。而所謂的routing屬性,是一個字典,內容是該裝飾器工廠函數的關鍵字參數,另加別的一些內容。
字典routing的鍵routes對應的值是一個列表,里面存放urls,也就是說,一個視圖方法func可以對應多個url。但endpoint是和mv一一映射的,所以所有的rule都是使用同一個endpoint
這樣,rule初始化的參數就都有了!
關于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是一個可調用類,執行時本質上是調用視圖函數mv,也就是說,只是視圖函數的一個簡單包裝而已。

調用階段的主邏輯如下:

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的實例是可調用對象,就是WSGI協議中的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和一些參數。
具體的執行視圖函數語句在request.dispatch()方法中:result = self._call_function(self.params)

八、一點小比較

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


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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

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