flask_route

一个 Hello world 的例子说明 route 的方法

首先我们以最常见的 hello, world 例子开始说明:

# app.py
from flask import Flask


app = Flask(__name__)


@app.route('/')
def index():
    return 'Hello, world.'


if __name__ == '__main__':
    app.run(debug=True)

这里我们使用了 route 这个装饰器来路由 / 这个地址。当然你也可以使用 add_url_rule 来实现相同的效果。

def index():
    return 'Hello, world.'
app.add_url_rule('/', 'index', index)

而实际上, route 内部正是调用了 add_url_rule,从 flask 的 route 的源码 中可以看到:

def route(self, rule, **options):
    def decorator(f):
        endpoint = options.pop('endpoint', None)
        self.add_url_rule(rule, endpoint, f, **options)
        return f
    return decorator

底层 route 的具体实现

现在我们知道可以使用 routeadd_url_rule 来路由,并且 route 底层就是调用了 add_url_rule, 那么 add_url_rule 到底做了什么咩。add_url_rule 的源码中, 可以发现以下重要线索:

@setupmethod
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
    ...
    rule = self.url_rule_class(rule, methods=methods, **options)
    ...
    self.url_map.add(rule)
    ...

进一步追踪得到,url_rule_class = werkzeug.routing.Ruleurl_map = werkzeug.routing.Map()

Map 里存了许多 URL 的规则,但是如何与 URL 关联起来呢?你还需要 werkzeug.routing.MapAdapter。也就是说, URL rule 加入到 Map 中,Map 通过调用 bind 或者 bind_to_environ 返回一个 MapAdapter。MapAdapter 可以使用 match 方法来匹配地址,成功的话,返回 (endpoint, view_arguments),知道了 endpoint 你也就可以知道对应的 view_fucntion 了。大致的路由情况基本是这样。

举个例子来说明一下:

>>> from app import app
>>> url_adapter = app.test_request_context().url_adapter
>>> url_adapter.match('/')
('index', {})
>>> @app.route('/<name')
    def hello(name):
        return "Hello, {0}".format(name)
>>> url_adapter.match('/Jhon')
('hello', {'name': u'Jhon'})
>>> with app.app_context():
...     make_response(app.view_functions['index']())
...
<Response 13 bytes [200 OK]>

说明到这里以后,你就可以使用更加底层的 Rule 和 Map 来实现路由的绑定。还是以 hello world 的例子来说明:

from flask import Flask
from werkzeug.routing import Rule, Map


app = Flask(__name__)

@app.endpint('index')
def index():
    return 'Hello, world.'

url_map = Map(
    [Rule('/', endpoint='index')],
)
app.url_map = url_map


if __name__ == '__main__':
    app.run(debug=True)

带有变量的 url pattern 和自定义的 converter

刚开始的 hello world 例子是最简单的,它是静态的,我们当然也可以用来路由动态的,比如说:

@app.route('/<name>')
def index(name):
    return 'Hello, {0}.'.format(name)

你还可以使用 converter,具体的用法是这样 <converter:variable_name>, 具体的作用呢,限制变量的类型。 比如说你定义的一个 url pattern 是 /page/<int:page_num>, 那么 /page/10 是可以正常使用的, 但是如果是 /page/hello 则会是 404 Not Found

flask 自带的 converter 有一下几种:

也即 werkzeug 自带的 converters,详细的文档在这里: Builtin Converters, Flask 的文档中也有相应的较为简单的说明: Variable Rules

其实你可以在 python 解释中自己动手看看, 假设我们的 hello world 的 app 的 python 文件名是 app.py:

>>> from app import app
>>> app.url_map.converters
{'default': <class 'werkzeug.routing.UnicodeConverter'>,
 'string': <class 'werkzeug.routing.UnicodeConverter'>,
 'int': <class 'werkzeug.routing.IntegerConverter'>,
 'path': <class 'werkzeug.routing.PathConverter'>,
 'float': <class 'werkzeug.routing.FloatConverter'>,
 'any': <class 'werkzeug.routing.AnyConverter'>,
 'uuid': <class 'werkzeug.routing.UUIDConverter'>}

虽然 werkzeug 已经自带了一些 converter,但是可能在实际的使用过程中还是无法满足我们的要求,这时候, 我们可以自己动手来定义一个 converter。在 flask 的 url_map 的文档中有提到具体的方法 (补充: werkzeug 中也有相关说明 Custom Converters):

from werkzeug.routing import BaseConverter

class ListConverter(BaseConverter):
    def to_python(self, value):
        return value.split(',')
    def to_url(self, values):
        return ','.join(BaseConverter.to_url(value)
                        for value in values)

app = Flask(__name__)
app.url_map.converters['list'] = ListConverter

这个时候呢,你可以用 /items/<list:items> 来定义路由规则。举个例子说, 访问 /item/egg, bread 时,你的 items 变量将是 ['egg', 'bread']

使用 URL Processors

现在你已经会使用变量和 converter 来动态地定义路由规则,但是有时候会碰到一些大量重复的情况。举个 flask 官网的例子, 在每个 view func 中,你都要把 <lang> 中的变量赋值给 g.lang, 这样的大量重复不利于代码修改和维护,写起来也麻烦。

@app.route('/<lang_code>/')
def index(lang_code):
    g.lang_code = lang_code
    ...

@app.route('/<lang_code>/about')
def about(lang_code):
    g.lang_code = lang_code
    ...

也许你能够想到自定义一个装饰器来避免重复冗余的代码,但是,在生成 URL 的时候(使用 url_for ),你还是要带上参数。

对于第一个问题, 你可以使用 url_value_preprocessor 来帮你做这件事情。事实上,按照字面意思你也能够猜出它的作用来了, URL 的预处理。 摘录一段官网文档的说明,来 解释和说明一下它的用途:

They are executed right after the request was matched and can execute code based on the URL values. The idea is that they pull information out of the values dictionary and put it somewhere else.

那么解决之前的问题就可以处理成为:

@app.url_value_preprocessor
def pull_lang_code(endpoint, values):
    g.lang_code = values.pop('lang_code', None)

也许你对 values 感到不解,我举个例子就好啦: 你的路由规则是 /<name>/<int:age>, 当真实的 URL 是 '/Jhon/18', 那么这时 values 就是 {'name': 'Jhon', 'age': 18}

对于生成 URL 的问题,你可以使用 url_defaults 来避免。

@app.url_defaults
def add_language_code(endpoint, values):
    values.setdefault('lang_code', g.lang_code)

也就是说,你可以不用每次 url_for('view_func', lang_code='blabla'), 它有 default value 啦。在你不想手动传入 lang_code 的时候,直接使用 url_for('view_func') 就可以,lang_code 的值会根据 g.lang 自动注入哒。用文档的一句话来说明它的作用:

They can automatically inject values into a call for url_for() automatically.

flask 关于 Using URL Processors 官方文档也建议再看看。