參考:
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由于限制較多,沒這個場景。
最后的吐槽:搞了半天,還是正則匹配。