Flask帅气的Hello World示例让人印象深刻。其中使用装饰器的路由写法功不可没,甚至以前看过一个系列没那么神奇,讲了app.route。是的,它好像并没有那么神奇,我也来解析一下😆

Flask的路由是对werkzeug进行了简单的包装,做成了app.route装饰器和blueprint。其实werkzeug中有一段docstring就能反映出flask的路由机制(MapAdapter.dispatch)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from werkzeug.wrappers import Request, Response
from werkzeug.wsgi import responder
from werkzeug.routing import Map, Rule
def on_index(request):
return Response('Hello from the index')
url_map = Map([Rule('/', endpoint='index')])
views = {'index': on_index}
@responder
def application(environ, start_response):
request = Request(environ)
urls = url_map.bind_to_environ(environ)
return urls.dispatch(lambda e, v: views[e](request, **v),
catch_http_exceptions=True)

werkzeug路由的全部实现都在routing.py文件当中,看它就够了。路由的基本功能可以说是

  1. 给出一个URL,匹配到它对应的处理函数,
  2. 给出函数能反推构造出URL

Rule

一个Rule就是一条URL匹配规则,它的模式如下<converter(arguments):name>,使用正则匹配找出我们需要的信息

1
2
3
4
5
6
7
8
9
10
11
_rule_re = re.compile(r'''
(?P<static>[^<]*) # static rule data
<
(?:
(?P<converter>[a-zA-Z_][a-zA-Z0-9_]*) # converter name
(?:\((?P<args>.*?)\))? # converter arguments
\: # variable delimiter
)?
(?P<variable>[a-zA-Z_][a-zA-Z0-9_]*) # variable name
>
''', re.VERBOSE)

大概是这么个样子rule匹配
werkzeug_rule_re
convert规则匹配
werkzeug_converter_args_re
很容易想到使用这2个正则能够匹配一个URL,一看他是否匹配某个rule,其次如果匹配那么提取出rule中的参数和值,比如/<int:id>就能匹配/16这个url且得到id=16。这2个函数就是match和build了
仅仅有它们是不够的。我们需要的不仅仅是一条Rule,我们需要的是所有Rule的集合。当我们每创建一个Rule的时候将它加入到集合中。所以它提供了get_rules和bind方法

Rule对象同时有2个非常重要的属性endpoint和_regex(编译后的正则).endpoint不用说匹配返回它,根据它返回构建好的URL,匹配功能则是由_regex对象提供的。另外有几个对象继承自Rule

1
2
3
Subdomain self.subdomain
Submount self.rule = self.path + self.rule
EndpointPrefix self.endpoint = self.prefix + self.endpoint

可以看出这三个对象都是对Rule的属性进行了小幅度修改,其中subdomain和submount修改的最终结果就是改变了_regex对象

说一下Subdomain,子域名。这玩意儿好像并不常用,一般一个子域名就是单独一个项目了,不大会几个子域名混合在一个项目里面。如果要使用子域名需要设置SERVER_NAME。示例如下

1
2
3
4
5
6
7
from flask import Flask
app = Flask(__name__)
app.config['SERVER_NAME'] = 'app.local:5000'
@app.route('/',subdomain='blog')
def index():
return 'Hello'
app.run('app.local')

然后就是在host里面添加就可以正常调试子域名了
1
2
127.0.0.1 app.local
127.0.0.1 blog.app.local

Map

Map的作用就是将所有的Rule组合在一起,所以add函数是必须的,Rule对象中的match和build都只是针对自己的,Map中提供的match和build则是针对所有Rule的。比如在Flask中写app.route必然会调用add,将该Rule添加到Map中。代码如下(0.1版本)

1
2
3
4
5
6
7
8
9
10
11
def add_url_rule(self, rule, endpoint, **options):
options['endpoint'] = endpoint
options.setdefault('methods', ('GET',))
self.url_map.add(Rule(rule, **options))
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

代码中self.url_map = Map(),可以看到app.route做了2件事情,加入到Map中,加入到了view_functions字典中(endpoint对应函数,这段代码和文章开头的代码是很相似的)

Map对象中有2个属性也是比较重要的,_rules列表和_rules_by_endpoint字典。前者存放所有Rule对象,匹配的时候用来遍历,后者为endpoint到Rule的映射。用来build重组URL

现在再来看看Flask中的dispatch分发逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def dispatch_request(self):
"""Does the request dispatching. Matches the URL and returns the
return value of the view or error handler. This does not have to
be a response object. In order to convert the return value to a
proper response object, call :func:`make_response`.
"""
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)

匹配请求得到endpoint再从view_function中得到对应的处理函数执行

Blueprint

@app.route虽然够炫酷,可是它绝不适应于大项目。因为注册的endpoint就是函数的名称,在大项目中绝对会存在重名函数的。Blueprint的解决办法就是

1
2
3
4
5
blueprint = Blueprint('public', __name__, static_folder='../static')
@blueprint.route('/', methods=['GET', 'POST'])
def index()
app.register_blueprint(blueprint)

这样它注册的endpoint就会变成public.index,解决了重名问题

有人问为什么endpoint不直接使用对应的函数,而要使用字符串。我认为是使用url_for等构建URL函数的时候需要它是字符串,因为使用装饰器后func = deactor(func),你使用@app.route之后注册的func和你现在的func不是同一个对象。构建函数将无法使用,这也告诉了我们,任何时候其他的装饰器都需要写在@app.route下面,因为如果写在上面,你注册的那个函数是不包含上面的装饰器的,注定了上面的装饰器无法执行