美文网首页
深入理解Flask路由(2)- werkzeug 路由系统

深入理解Flask路由(2)- werkzeug 路由系统

作者: Stone0823 | 来源:发表于2020-04-05 08:30 被阅读0次

    上一篇我们说到:Flask 的路由机制是在 werkzeug 中实现的, Flask 只是调用而已。Flask 的路由包括三个主要过程:

    • 路由的构建 (werkzeug 实现相关的数据结构,Rule, Map 等)
    • 路由的匹配 (werkzueg 实现)
    • 请求的分派(dispatch, Flask 实现)

    本篇先介绍 werkzeug 两个重要的数据结构 Rule 和 Map,后续再把三个阶段串起来。

    Flask 的路由主要是根据前端请求 (request) 中 url path 信息,找到对应的 endpoint,再根据 endpoint 找到对应的 view function 进行处理。在这个关系中,url 与 endpoint 的映射,由 werkzeug 的 Rule 和 Map 来实现,endpoint 与 view function 的映射由 Flask 用 Python 标准数据结构 dict 实现。 在 werkzeug 中,Rule 表示 url rule 和 endpoint 的映射,Map 实现与多个 Rule 的绑定。看下面的一段代码:

    from werkzeug.routing import Map, Rule
    
    url_rules = [
        Rule('/', endpoint='index', methods=['GET']),
        Rule('/about', endpoint='about', methods=['GET'])
    ]
    
    map = Map(url_rules)
    
    print(map)
    

    在这段代码中,我们定义了两个 rule rule,然后与 Map 绑定,运行代码,打印出下面的结果:

    Map([<Rule '/about' (HEAD, GET) -> about>, 
         <Rule '/' (HEAD, GET) -> index>])
    

    Rule

    Rule 定义 url rule 和 endpoint 之间的映射,url rule 与 endpoint 是多对一关系。通过 Rule 的初始化方法 __init__() 可以知道如何创建一个 Rule 实例。下面介绍主要知识点,而不说明每一个细节。

    def __init__(
        self,
        string,
        defaults=None,
        subdomain=None,
        methods=None,
        build_only=False,
        endpoint=None,
        strict_slashes=None,
        redirect_to=None,
        alias=False,
        host=None,
    ):
        # 代码略
    

    Rule 的第一个参数 string (str 类型),表示 url 的路径规则 (url rule),比如刚才的例子中 '/' 和 ‘/about’。rule 的标准格式是 <converter(arguments):name>,由转换器 (converter)、转换器参数(argument) 和名称(name)构成。converter 在路由匹配的时候会用到,这里暂且不表。converter 可以省略,默认值为 UnicodeConverter

    Url rule 必须从 / 开始。/ 的英文为 slash,了解其英文有助于看懂代码。如果不以 slash 开始,抛出如下错误:

    # FILE: werkzeug/routing.py
    
    if not string.startswith("/"):
        raise ValueError("urls must start with a leading slash")
    

    url path 的结尾,有以 / 结尾和不以 / 结尾两种可能 。比如有些人用 /about 表示,有些用 /about/ 表示(多了一个斜杠)。其实这两个应该是同一个 url。为了保证 url 的唯一性,避免被搜索引擎索引两次,Rule 和 Map 提供了相关属性对此进行区分:

    is_leaf 属性:如果 url 以 / 结束,则表示这个 url 是 branch url (枝),否则就是 leaf url (叶), is_leaf = True。Rule 还有另外一个 strict_slash 属性(Map 也有 strict_slash 属性,默认为 True)。如果 strict_slash 为 True, 则 url 不以 / 结尾时重定向到以 / 结尾的 url。我们用一段代码来说明 strict slash 和重定向的关系:

    from flask import Flask
    from werkzeug.routing import Map, Rule
    
    app = Flask(__name__)
    
    @app.route('/')
    def index():
        return 'Index Page'
    
    @app.route('/about/')
    def about():
        return 'About Page'
    
    if __name__ == '__main__':
        app.run()
    

    在这种情况下,如果访问 /about 会重定向到 /about/。因为 strict_slash 默认为 True。

    127.0.0.1 - - [30/Mar/2020 22:19:45] "GET /about HTTP/1.1" 308 -
    127.0.0.1 - - [30/Mar/2020 22:19:45] "GET /about/ HTTP/1.1" 200 -
    

    如果将装饰器改为 /about

    @app.route('/about')
    def about():
        return 'About Page'
    

    此时访问 /about 成功,而访问 /about/ 则出现 404 Not Found 错误:

    127.0.0.1 - - [30/Mar/2020 22:21:34] "GET /about HTTP/1.1" 200 -
    127.0.0.1 - - [30/Mar/2020 22:21:40] "GET /about/ HTTP/1.1" 404 -
    

    如果我们将 app.url_map.strict_slash 改为 False, 则 /about/about/ 能分别被访问。不建议这样用,因为违背了唯一 url 原则。

    app = Flask(__name__)
    app.url_map.strict_slashes = False # 添加一句代码,默认值为True
    
    127.0.0.1 - - [30/Mar/2020 22:24:19] "GET /about HTTP/1.1" 200 -
    127.0.0.1 - - [30/Mar/2020 22:24:24] "GET /about/ HTTP/1.1" 200 -
    

    methods 指 rule 适用的 HTTP method,比如 GET, POST 等。如果不指定 method,则所有的 method 都被允许。如果方法中有 GET ,则 HEAD 方法被自动添加。methods 参数可以是 list, tupple 或 set 这样的集合,通常是 list。看看 Rule.__init__() 方法的代码,理解这些要点:

    # FILE: werkzeug/routing.py
    
    # Rule.__init__() method:
    if methods is not None:
        if isinstance(methods, str):
            raise TypeError("'methods' should be a list of strings.")
    
        methods = {x.upper() for x in methods}
    
        if "HEAD" not in methods and "GET" in methods:
            methods.add("HEAD")
    
        if websocket and methods - {"GET", "HEAD", "OPTIONS"}:
            raise ValueError(
                "WebSocket rules can only use 'GET', 'HEAD', and 'OPTIONS' methods."
            )
    
    self.methods = methods
    

    Map

    Map 绑定多个 Rule,在本篇开始的示例代码中,我们是这样创建 Map 实例的:

    map = Map(url_rules)
    

    之所以可以这样构造,是因为 Map.__init__() 方法自动调用了下面一系列方法:

    - Map.add() :  封装方法,调用 Rule.bind()
    - Rule.bind() : 核心代码,提供 Map 与 Rule 绑定的实现
    

    下面列出主要的代码:

    # Map.__init__()
    
    for rulefactory in rules or ():
        self.add(rulefactory)
    

    下面是 Map.add() 方法的代码:

    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
    

    继续看 rule.bind() 的代码。bind() 方法执行核心的操作将 rule 绑定到 Map, 并且基于 rule 创建正则表达式,保存在 Map._rules 属性中

    def bind(self, map, rebind=False):
        """Bind the url to a map and create a regular expression based on
        the information from the rule itself and the defaults from the map.
    
        :internal:
        """
        if self.map is not None and not rebind:
            raise RuntimeError("url rule %r already bound to map %r" % (self, self.map))
        self.map = map
        if self.strict_slashes is None:
            self.strict_slashes = map.strict_slashes
        if self.subdomain is None:
            self.subdomain = map.default_subdomain
        self.compile()
    

    了解了 Rule 和 Map 的实现细节,我们接下来不用装饰器和 add_url_rule() 方法来实现 Flask 路由,以加深对相关知识点的理解。

    首先,简单来说,Flask 路由信息包括 url_mapview_functionsFlask.url_map 属性是 Map 的实例, 我们将 Map 传给它就可以了。view_functions是 dict,可以直接创建。示例代码如下:

    from flask import Flask
    from werkzeug.routing import Map, Rule
    
    app = Flask(__name__)
    
    def index():
        return 'Index Page'
    
    def about():
        return 'About Page'
    
    # create url rules and map instances
    url_rules = [
        Rule('/', endpoint='index', methods=['GET']),
        Rule('/about', endpoint='about', methods=['GET'])
    ]
    map = Map(url_rules)
    
    # set app.url_map and view_functions
    app.url_map = map
    app.view_functions = {
        'index': index,
        'about': about
    }
    
    if __name__ == '__main__':
        app.run()
    

    但这段代码有一个小问题。我们知道 Flask 为了能处理静态文件,在.__init__() 方法中构造了一个 static rule,上面的代码直接对 url_map 赋值,初始化方法中创建的 static rule 就被丢掉了。为了保留 static rule,可以模拟 Map 添加 Rule 的方式,稍作变更,以下是代码,我略去了重复的部分。

    url_rules = [
        Rule('/', endpoint='index', methods=['GET']),
        Rule('/about', endpoint='about', methods=['GET'])
    ]
    
    for rule in url_rules:
        app.url_map.add(rule)
    
    app.view_functions['index'] = index
    app.view_functions['about'] = about
    

    一般情况下,我们并不需要用这种方式来编写 Flask 路由代码,本示例仅仅为了理解机制特意为之,回过头来,我们再去看看 Flask.add_url_rule() 方法,可以看到 Flask 也是这么做的:

    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)
        
        # 添加view functions
        if view_func is not None:      
           self.view_functions[endpoint] = view_func
    

    Map 另外一个核心知识点 converter 下篇再讲,待续。

    参考

    相关文章

      网友评论

          本文标题:深入理解Flask路由(2)- werkzeug 路由系统

          本文链接:https://www.haomeiwen.com/subject/oasyphtx.html