参考:
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,通过rule的bind方法来与map实例关联。并且,在map的_rules属性中插入rule实例,在_rules_by_endpoint属性中,创建rule.endpoint和rule实例的关联。
具体代码如下:
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_info,request_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)),从而返回rule的endpoint和一些参数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,另两个关键关键参数是endpoint和methods。
get_rules(self, map)
返回本身。
bind(self, map, rebind=False)
将自身与map绑定。
调用compile方法,依据rule和map,创建一个正则表达式。这其实就是绑定的实质。
compile(self)
依据rule和map二者的信息,创建一个正则表达式,用于后续匹配。
match(self, path, method=None)
进行匹配。
四、Endpoint
Werkzeug本身不定义Endpoint。这个类主要的作用是将Rule与最终用于处理的视图函数进行关联。从上述内容可知,顺序应该是:url→rule→endpoint→view_func。但最后一步具体怎么做,Werkzeug是不管的。
五、整体流程
构建阶段:
- 创建Map实例map。
- 不论是在map初始化时,还是直接调用map.add,将map与Rule实例rule关联。
- rule初始化时需要传入url和endpoint。
- 在map.add方法中,rule会调用bind方法,与map绑定。
- 在rule.bind的方法中,会调用compile方法,生成一个正则表达式,用于后续的匹配。
匹配阶段:
- map使用方法bind_to_environ与environ关联。
- 方法bind_to_environ调用bind方法,返回一个MapAdapter实例map_adapter。
- 调用map_adapter的match方法,判断是否有与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),endpoint,f(视图函数)。
在Flask中,endpoint默认定义为f的name。
从帮助文档可以看出,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中,rule与url_map进行了绑定。
Flask中endpoint和view_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_map的bind方法,来返回一个MapAdapter实例,设置为RequestContext的url_adapter属性。
接着调用match_request方法,本质就是调用url_adapter的match方法,找到对应的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,找到所有的route→func关系,为它们创建rule实例,加到map中。
具体而言,找到类型为controller的类cls,再找到cls的方法。
若某一方法mv有routing属性,则该方法确定是被装饰器工厂函数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中视图函数一旦使用装饰器,那么立马就会创建rule与app.rule_map进行绑定,比较灵活。而Odoo就比较挫,要统一进行遍历。
但是Flask的url处理就比较简单,一个view_func只能对应一个url,这点就不如Odoo。
Flask有Blueprint可以灵活处理视图函数,所谓的app.register_blueprint本质上还是调用app的add_url_rule方法。Odoo由于限制较多,没这个场景。
最后的吐槽:搞了半天,还是正则匹配。