在上一篇中,从最简单使用形式入手,简单的过了一遍Flask应用启动流程以及其背后的原理,本篇将会以类似的风格剖析Flask路由相关的内容,同样不会涉及过多细节,力求从较高的维度去看。
Flask版本:1.0.2
回归一下上一篇文章,在通过@app.route()装饰器将函数转为Flask视图函数时,多次提及了endpoint,对应的add_url_rule()代码如下。
# flask/app.py/Flask
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
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
在add_url_rule()方法中通过endpoint,将路由与视图函数关联在一起,为什么不直接将路由与视图函数关联?要多弄个endpoint?
为了回答这个问题,先来了解一下endpoint。
通常,可以通过两种方式将路由与视图函数关联。
@app.route('/hello/' )
def hello(name):
return f'Hello, {name}!'
或
def hello(name):
return f'Hello, {name}!'
app.add_url_rule('/hello/' , 'hello', hello)
本地运行起来,接着访问localhost:5000/hello/二两
,就会调用hello()方法,此时关联方式为:localhost:5000/hello/二两
-> endpoint:hello
-> hello()方法
这是最简单的写法,endpoint与方法名相同,可以通过endpoint参数修改endpoint名称。
@app.route('/hello/', endpoint='sayhello')
def hello(name):
return f'Hello, {name}!'
此时,关联改变为localhost:5000/hello/二两
-> endpoint:sayhello
-> hello()方法
。
通过endpoint可以快速构建url,不再需要对url进行硬编码。
@app.route('/')
def index():
# 将访问 hello/二两
print url_for('hello', name='二两')
ok,ok,我明白了endpoint是做什么的,但还是一开始的问题,为什么要endpoint?路由与函数直接对应上不就好了?
因为使用endpoint更方便,可以将所有后台管理的逻辑都放在admin endpoint下,将所用用户相关的放在user endpoint下,当然这要配合蓝图机制来使用。
# main.py:
from flask import Flask, Blueprint
from admin import admin
from user import user
app = Flask(__name__)
# 注册蓝图
app.register_blueprint(admin, url_prefix='admin')
app.register_blueprint(user, url_prefix='user')
# admin.py:
# 实例化蓝图
admin = Blueprint('admin', __name__)
@admin.route('/home')
def home():
return 'Hello, root user!'
# user.py:
user = Blueprint('user', __name__)
@user.route('/home')
def home():
return 'Hello, lowly normal user!'
# 使用时
print url_for('admin.home') # Prints '/admin/home'
print url_for('user.home') # Prints '/user/home'
理解endpoint是理解Flask路由机制的前提,不然,当你浏览Flask路由机制匹配规则会比较蒙圈。
路由机制关键在于匹配,而匹配的逻辑在dispatch_request()方法中,该方法的调用路径为:__call__() -> wsgi\_app() -> full_dispatch_request() -> dispatch_request()
,方法代码如下。
# flask/app.py
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
if getattr(rule, 'provide_automatic_options', False) \
and req.method == 'OPTIONS':
return self.make_default_options_response()
# 通过endpoint获得相应的视图函数
return self.view_functions[rule.endpoint](**req.view_args)
从_request_ctx_stack上下文中获得当前请求的上下文,找到当前请求的路由并从中找到endpoint,再通过endpoint找到对应的视图函数。
关键在于req.url_rule
与rule.endpoint
这两个变量怎么来的?
_request_ctx_stack变量涉及上下文相关的内容,细节先不提,其中存储着RequestContext类的实例对象,该对象与路由匹配相关的代码如下。
# flask/ctx.py
class RequestContext(object):
def __init__(self, app, environ, request=None, session=None):
self.app = app
if request is None:
# 将environ转为request
request = app.request_class(environ)
self.request = request
self.url_adapter = None
try:
self.url_adapter = app.create_url_adapter(self.request)
except HTTPException as e:
self.request.routing_exception = e
self.flashes = None
self.session = session
# ... 省略无关代码
def match_request(self):
"""Can be overridden by a subclass to hook into the matching
of the request.
"""
try:
# 匹配url
result = self.url_adapter.match(return_rule=True)
# 返回结果
self.request.url_rule, self.request.view_args = result
except HTTPException as e:
self.request.routing_exception = e
在__init__()中,通过app.request_class()方法,将environ转为Request类实例,接着使用app.create_url_adapter()方法将request相关信息存到url_map变量中。
在路由匹配时会调用match_request()方法,该方法具体的匹配规则又由 self.url_adapter.match()方法完成,该方法会返回url_rule与view_args。
为了进一步理解,剖析一下create_url_adapter()方法与match()方法,先看create_url_adapter()方法,一层层看下去,该方法的调用顺序为:create_url_adapter() -> self.url_map.bind_to_environ() -> Map.bind() -> MapAdapter()
,简单而言,该方法最后返回一个MapAdapter类实例,MapAdapter类下就有match()方法,上面self.url_adapter.match()调用的就是这个方法,该方法实现具体的匹配逻辑,最终返回返回url_rule与view_args(路由与视图函数的参数)
# werkzeug/routing.py/MapAdapter
def match(self, path_info=None, method=None, return_rule=False, query_args=None):
for rule in self.map._rules:
try:
rv = rule.match(path, method)
except:
# ... 省略
# 返回 路由与视图函数的参数
if return_rule:
return rule, rv
else:
return rule.endpoint, rv
match()匹配规则的逻辑比较细节,有兴趣的可以去werkzeug的routing.py文件中查阅。
至此,Flask路由的大致过程就分析完了,简单总结一下:
over!
Flask路由相关的内容就简单剖析完了,后面将接着分析上下文、请求与相应相关的内容,希望喜欢。
如果本篇文章对你有些帮助,麻烦点一下「在看」支持二两,下篇文章见。