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由于限制较多,没这个场景。


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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容

  • 本文大致梳理一下Flask框架在处理url路由时的主要过程。 类图 route装饰器 在Flask应用中,我们一般...
    Jakiro阅读 3,525评论 0 6
  • flask源码分析 1. 前言 本文将基于flask 0.1版本(git checkout 8605cc3)来分析...
    甘尼克斯_阅读 2,695评论 1 0
  • 3.flask 源码解析:路由 构建路由规则 一个 web 应用不同的路径会有不同的处理函数,路由就是根据请求的 ...
    火鸡不肥阅读 554评论 0 0
  • 通常有3种定义路由函数的方法: 使用flask.Flask.route() 修饰器。 使用flask.Flask....
    黄智勇atTAFinder阅读 6,254评论 0 5
  • 一声猿啸山河远 万里狂歌日月长 独棹江湖风浪险 飞刀做桨探花郎 一聲猿嘯山河遠 萬里狂歌日月長 獨棹江湖風浪險 飛...
    诗呆阅读 4,092评论 62 161