序言
上篇文章写了 Flask 的请求响应循环,详情请看 源码阅读 – Flask v0.1 [03] Flask 请求响应循环 ,这篇文章写 Flask 的路由系统 。
路由系统
注册路由
路由系统内部是由 Werkzeug 实现的 , 为了更好地了解 Flask 中的相关代码 , 需要先看一下路由功能在 Werkzeug 中是如何实现的 。 下面的代码用于创建路由表 Map , 并添加三个 URL 规则 :
>>> from werkzeug.routing import Map, Rule
>>> m = Map()
>>> rule1 = Rule('/', endpoint='index')
>>> rule2 = Rule('/downloads/', endpoint='downloads/index')
>>> rule3 = Rule('/downloads/<int:id>', endpoint='downloads/show')
>>> m
Map([[]])
>>> m.add(rule1)
>>> m.add(rule2)
>>> m.add(rule3)
>>> m
Map([[<Rule '/' -> index>,
<Rule '/downloads/' -> downloads/index>,
<Rule '/downloads/<id>' -> downloads/show>]])
>>>
在 Flask 中 , 我们使用 route()
装饰器来将试图函数注册为路由 :
@app.route('/')
def hello():
return 'Hello, Flask!'
Flask.route()
是 Flask 类的类方法 , 如代码清单所示 :
[flask.py]
class Flask(object):
def route(self, rule, **options):
def decorator(f):
self.add_url_rule(rule, f.__name__, **options)
self.view_functions[f.__name__] = f
return f
return decorator
可以看到 route 装饰器的内部调用了 add_url_rule()
来添加 URL 规则 , 所以注册路由也可以直接使用 add_url_rule 实现 。 add_url_rule() 方法如代码清单所示 :
[flask.py]
class Flask(object):
def add_url_rule(self, rule, endpoint, **options):
options['endpoint'] = endpoint
options.setdefault('methods', ('GET',))
self.url_map.add(Rule(rule, **options))
这个方法的重点是 self.url_map.add(Rule(rule, **options))
, 这里引入了 url_map
。 而在 route 函数中则引入了 view_functions
对象 。
url_map
是 Werkzeug 的 Map 类实例 (werkzeug.routing.Map
) 。 它存储了 URL 规则和相关配置 , 这里的 rule 是 Werkzeug 提供的 Rule 实例 (werkzeug.routing.Rule
) , 其中保存了端点和 URL 规则的映射关系 。
而 view_function
则是 Flask 类中定义的一个字典 , 它存储了端点和视图函数的映射关系 。 看到这里你大概已经发现端点是如何作为中间人连接起 URL 规则和视图函数的 。 如果回过头看本节开始提供的 Werkzeug 中的路由注册代码 , 你会发现 add_url_rule() 方法中的这些代码做了同样的事情 :
[flask.py]
self.url_map.add(Rule(rule, **options))
URL 匹配
在上面的 Werkzeug 路由注册代码示例中 , 我们创建了路由表 m , 并使用 add() 方法添加了三个路由规则 。 现在 , 来看看如何在 Werkzeug 中进行 URL 匹配 , URL 匹配的示例如下所示 :
>>> from werkzeug.routing import Map, Rule
>>> m = Map()
>>> rule1 = Rule('/', endpoint='index')
>>> rule2 = Rule('/downloads/', endpoint='downloads/index')
>>> rule3 = Rule('/downloads/<int:id>', endpoint='downloads/show')
>>> m
Map([[]])
>>> m.add(rule1)
>>> m.add(rule2)
>>> m.add(rule3)
>>> m
Map([[<Rule '/' -> index>,
<Rule '/downloads/' -> downloads/index>,
<Rule '/downloads/<id>' -> downloads/show>]])
>>> urls = m.bind('example.com')
>>> urls.match('/', 'GET')
('index', {})
>>> urls.match('/downloads/42')
('downloads/show', {'id': 42})
>>> urls.match('/downloads')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\Anaconda3\envs\python27\lib\site-packages\werkzeug\routing.py", line 1261, in match
url_quote(path_info.lstrip('/'), self.map.charset)
werkzeug.routing.RequestRedirect: 301: Moved Permanently
>>> urls.match('/missing')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\Anaconda3\envs\python27\lib\site-packages\werkzeug\routing.py", line 1302, in match
raise NotFound()
werkzeug.exceptions.NotFound: 404: Not Found
>>>
Map.bind()
方法和 Map.bind_to_environ()
都会返回一个 MapAdapter
对象 , 它负责匹配和构建 URL 。 MapAdapter 类的 match 方法用来判断传入的 URL 是否匹配 Map 对象中存储的路由规则 (存储在 self.map._rules
列表中) 。 上面的例子中分别展示了几种常见的匹配情况 。 匹配成功后会返回一个包含 URL 端点和 URL 变量的元组 。
为了确保 URL 的唯一 , Werkzeug 使用下面的规则来处理尾部斜线问题 : 当你定义的 URL 规则添加了尾部斜线时 , 用户访问未加尾部斜线的 URL 时会被自动重定向到正确的 URL ; 反过来 , 如果定义的 URL 不包含尾部斜线 , 用户访问的 URL 添加了尾部斜线则会返回 404 错误 。 MapAdapter 类的 build() 方法用于创建 URL , 我们用来生成 URL 的 url_for() 函数内部就是通过 build() 方法实现的 。 下面是一个简单的例子 :
接着上文 :
>>> urls.build('index', {})
'/'
>>> urls.build('downloads/show', {'id': 42})
'/downloads/42'
>>> urls.build('downloads/show', {'id': 42}, force_external=True)
'http://example.com/downloads/42'
>>>
关于 Werkzeug 的路由系统 , 这里只是简单介绍 , 具体可以查看 Werkzeug 的文档 (http://werkzeug.pocoo.org/docs/latest/routing/) 及相关代码 。
在上一小节 , 注册路由后 , 两个对应关系分别存储到 url_map
和 view_functions
中 , 前者存储了 URL 到端点的映射关系 , 后者则存储了端点和视图函数的映射关系 。 下面我们会了解在客户端发送请求时 , Flask 是如何根据请求的 URL 找到对应的视图函数的 。 在上文分析 Flask 中的请求响应循环时 , 我们曾说过 , 请求的处理最终交给了 dispatch_request() 方法 , 这个方法如代码清单所示 :
[flask.py]
class Flask(object):
def dispatch_request(self):
try:
endpoint, values = self.match_request()
return self.view_functions[endpoint](**values)
except HTTPException, e:
handler = self.error_handlers.get(e.code)
if handler is None:
return e
return handler(e)
except Exception, e:
handler = self.error_handlers.get(500)
if self.debug or handler is None:
raise
return handler(e)
从名字可以看出来 , 这个方法负责请求调度 (request dispatching) 。 正是 dispatch_request()
方法实现了从请求的 URL 找到端点 , 再从端点找到对应的视图函数并调用的过程 。 view_functions 在注册路由时 , 由 Rule 类表示的 rule 对象由 route()
装饰器传入的参数创建 。 如上文中的描述 : view_function
是 Flask 类中定义的一个字典 , 它存储了端点和视图函数的映射关系 。
而这里先调用 match_request()
方法得到处理的 endpoint 和 values , 以如下最简单的示例代码 , 如果我请求的是 http://localhost:5000/hello/world/test , 则结果为 : endpoint=’hello_test’ , values={‘name’:’world’} , 调试信息如下图 :
[直接在flask.py文件中添加代码测试]
app = Flask(__name__)
@app.route('/hello/<name>/test', methods=['POST', 'GET'])
def hello_test(name):
if name == "Test":
return 'Test'
else:
return 'hello'
@app.route('/hello/<name>', methods=['POST', 'GET'])
def hello(name):
if name == "Test":
return 'Test'
else:
return 'hello'
@app.route('/')
def index():
return "This is index page"
if __name__ == '__main__':
app.run()
如果我请求的是 http://localhost:5000/hello/world , 则结果为 : endpoint=’hello’ , values={‘name’:’world’} , 调试信息如下图 :
由此可见 endpoint 就是视图函数的名称 , values 则是注册路由时 , 路径的可变参数的名称与值组成的字典 。
那么我们来仔细看一下 view_functions 对象 , view_functions 在 Flask 对象初始化的时候是空字典 :
class Flask(object):
def __init__(self, package_name):
...
self.view_functions = {}
...
第一次出现变化的是在添加路由的时候 , 即在 Flask.route() 函数内部出现了首次变化 。
[flask.py]
class Flask(object):
def route(self, rule, **options):
def decorator(f):
self.add_url_rule(rule, f.__name__, **options)
self.view_functions[f.__name__] = f
return f
return decorator
将视图函数装饰一下 , 把视图函数本身对象复制给以视图函数名为 key , 形式如下 :
{'func_name': func(Object)}
因此在 dispatch_request 函数最后一行 return self.view_functions[endpoint](**values) 中 , self.view_functions[endpoint] 代表的是视图函数对象本身 , 后面的 (**values) 可以表示为 endpoint_obj(name=value) , 即是执行视图函数 。
虽然已经通过调试知道 match_request 函数的执行结果 , 但还需要通过源码理解一番 。
[flask.py]
class Flask(object):
def match_request(self):
rv = _request_ctx_stack.top.url_adapter.match()
request.endpoint, request.view_args = rv
return rv
通过上面的代码可以看到 , 最终是调用了请求的 match() 方法来获取到 endpoint 和参数 , 而调用者 url_adapter = url_map.bind_to_environ(environ) , 在 _RequestContext 类的初始化函数中可以看到 :
class _RequestContext(object):
def __init__(self, app, environ):
self.app = app
self.url_adapter = app.url_map.bind_to_environ(environ)
self.request = app.request_class(environ)
self.session = app.open_session(self.request)
self.g = _RequestGlobals()
self.flashes = None
self.url_adapter = app.url_map.bind_to_environ(environ)
, 也就是说实际获取 endpoint 与参数是通过调用 url_map.bind_to_environ(environ).match()
来获取的 。 通过前面的介绍我们已经知道 , url_map 中存储的是 url 与 endpoint 之间的映射关系 , 这种映射关系是通过 @app.route() 进行指定的 。 而 environ 为单次请求信息 , 内部包含请求的 url 。 可以理解为存储信息的对象 url_map 绑定特定的请求信息 environ , 然后进行匹配 match() , 即可得到请求对应的 endpoint 和参数 value 。 也因此说明 match_request 在本地上下文中使用 , 每次请求 url 创建请求上下午对象时都会执行该函数 。
可以看到 endpoint 和 view_args 属性就在这个方法中创建 。 这个方法调用了 _request_ctx_stack.top.url_adapter.match()
来获取 endpoint 和 view_args 。 这里的 url_adapter 属性在 _RequestContext
的构造函数中定义 , 其值为 app.url_map.bind_to_environ(environ)
[flask.py]
class _RequestContext(object):
"""The request context contains all request relevant information. It is
created at the beginning of the request and pushed to the
`_request_ctx_stack` and removed at the end of it. It will create the
URL adapter and request object for the WSGI environment provided.
"""
def __init__(self, app, environ):
self.app = app
self.url_adapter = app.url_map.bind_to_environ(environ)
self.request = app.request_class(environ)
self.session = app.open_session(self.request)
self.g = _RequestGlobals()
self.flashes = None
def __enter__(self):
_request_ctx_stack.push(self)
def __exit__(self, exc_type, exc_value, tb):
# do not pop the request stack if we are in debug mode and an
# exception happened. This will allow the debugger to still
# access the request object in the interactive shell.
if tb is None or not self.app.debug:
_request_ctx_stack.pop()
我们知道 url_map 属性是一个 Map 对象 , 可以看出它最后调用了 bind()
或 bind_to_environ()
方法 , 最终会返回一个 MapAdapter 类实例 。
match_request()
方法通过调用 MapAdapter.match() 方法来匹配请求 URL , 返回结果包含 endpoint 属性 , 存储着匹配成功的端点值 。
在 dispatch_request()
最后这一行代码中,通过在 view_functions 字典中根据端点作为键即可找到对应的视图函数对象 , 并调用它 :
return self.view_functions[endpoint](**values)
调用视图函数时传递的参数 **values
包含 URL 中解析出的变量值 , 也就是 match()
函数返回的第二个值 。 这时代码执行流程才终于走到视图函数中 。
结语
本节就先到此结束了,后面接着写。
如有错误,敬请指出。感谢指正! — 2021-05-22 16:28:17
最新评论
这个软件有bug的,客户端windows有些键不能用如逗号、句号
没有收到邮件通知
我的评论通知貌似坏掉了,定位一下问题
测试一下重新部署后的邮件功能
居然看到自己公司的MIB库,诚惶诚恐
那可能是RobotFramework-ride的版本问题。我装的1.7.4.2,有这个限制。我有空再尝试下旧版本吧,感谢回复。
你好!我在python2.7中安装RobotFramework-ride的时候提示wxPython的版本最高是2.18.12,用pip下载的wxPython版本是4.10,而且我在那个路径下没有找到2
真的太好了,太感谢了,在bilibili和CSDN上都找遍了,终于在你这里找到了