zoukankan      html  css  js  c++  java
  • Bottle源码阅读笔记(二):路由

    前言

    程序收到请求后,会根据URL来寻找相应的视图函数,随后由其生成页面发送回给客户端。其中,不同的URL对应着不同的视图函数,这就存在一个映射关系。而处理这个映射关系的功能就叫做路由。路由的实现分为两部分:
    1. 生成URL映射关系
    2. 根据请求匹配正确的视图函数
    本文将围绕这两个部分进行分析。

    生成URL映射关系

    在Bottle的示例程序中,我们使用@app.route修饰器来将地址'/hello'映射到视图函数hello:

    1 @app.route('/hello')
    2 def hello():
    3     return 'Hello World!'

    下面以'/hello'为例子来分析app.route的代码。

     1 def route(self, path=None, method='GET', callback=None, name=None,
     2           apply=None, skip=None, **config):
     3     """
     4         :param callback: An optional shortcut to avoid the decorator
     5           syntax. ``route(..., callback=func)`` equals ``route(...)(func)``
     6     """
     7     if callable(path): path, callback = None, path
     8     plugins = makelist(apply)
     9     skiplist = makelist(skip)
    10     def decorator(callback):
    11         if isinstance(callback, basestring): callback = load(callback)
    12         for rule in makelist(path) or yieldroutes(callback):
    13             for verb in makelist(method):
    14                 verb = verb.upper()
    15                 route = Route(self, rule, verb, callback, name=name,
    16                               plugins=plugins, skiplist=skiplist, **config)
    17                 self.add_route(route)
    18         return callback
    19     return decorator(callback) if callback else decorator

    注意注释和最后一行代码,这种形式的return意味着我们还可以使用app.route('/hello', callback=hello)来实现相同的功能。

    route方法将我们定下的路由规则('/hello')和与之相关的HTTP方法(默认为GET)、使用的插件等参数组合成一个Route路由对象,然后通过Router.add()将这个路由添加到处理映射关系的Router对象中。
    Router.add()部分代码:

    1 def add(self, rule, method, target, name=None):
    2     # do some something
    3     if is_static and not self.strict_order:
    4         self.static.setdefault(method, {})
    5         self.static[method][self.build(rule)] = (target, None)
    6         retun
    7     # dynamic path parse

    在Router对象中,它维护着两个字典:static和dyna_route,分别用来存储静态路由和动态路由(动态路由的映射与静态相似,这里按下不表)。以我们的示例程序为例:

    static = {
        'GET': {
            '/hello': hello,
        }
    }

    可以看出,Bottle最终是用一个字典来保存这个映射关系的,而且还添加了对HTTP方法的支持。所以在Bottle文档中看到可以用多个路由装饰器装饰同一个视图函数,也就不足为奇了。

    @app.route('/', method='POST')
    @app.route('/', method='GET')
    @app.route('/hello', method='GET')
    def func():
        pass
    
    static = {
        'GET': {
            '/': func,
            '/hello': func,
        }
        'POST': {
            '/': func,
        }
    }

    现在映射关系生成了,那么程序在处理请求的时候,它的内部是如何实现匹配的呢?

    匹配视图函数

    这里提个小插曲,之前在我阅读到这部分的时候,我还没有搞清楚WSGI是什么,所以我在分析这一步时并没有从Bottle.__call__开始,而是直接Ctrl+f寻找static来确定这个字典在哪里被调用了。虽然是个笨方法,但好歹找到了答案。:D
    在Router.match中,可以找到关于static的调用。match()先是从envrion变量中取出请求的URL和请求方法,然后直接从static中取值。

     1 def match(self, environ):
     2     ''' Return a (target, url_agrs) tuple or raise HTTPError(400/404/405). '''
     3     verb = environ['REQUEST_METHOD'].upper()
     4     path = environ['PATH_INFO'] or '/'
     5     target = None
     6     if verb == 'HEAD':
     7         methods = ['PROXY', verb, 'GET', 'ANY']
     8     else:
     9         methods = ['PROXY', verb, 'ANY']
    10 
    11     for method in methods:
    12         if method in self.static and path in self.static[method]:
    13             target, getargs = self.static[method][path]
    14             return target, getargs(path) if getargs else {}
    15         elif method in self.dyna_regexes:
    16             for combined, rules in self.dyna_regexes[method]:
    17                 match = combined(path)
    18                 if match:
    19                     target, getargs = rules[match.lastindex - 1]
    20                     return target, getargs(path) if getargs else {}

    接着我不断在用类似的方法寻找上级调用,最终画出了一个这样的函数调用关系链。

    以上内容很好地验证了我们上一篇所说的:请求的信息都存储在envrion中,以及Bottle.__call__是我们阅读源程序的入口。

    在处理输出的Bottle._handle()中,找到对应的路由后,直接调用路由的call方法,也就是我们的视图函数hello。

    1 def _handle(self, envrion):
    2     route, args = self.router.match(environ)
    3     return route.call(**args)

    错误页面

    如果程序出错了,Bottl会显示一个默认的错误页面,例如我们熟悉的404页面。

    在Bottle内部,对于错误页面的处理跟普通的页面差不多。在Bottle维护着一个专门处理错误页面映射关系的error_handler字典,不过它的键不是HTTP方法或者URL,而是HTTP错误状态码。
    类似地,Bottle有专门的@error装饰器让我们自定义错误页面。

    1 def error(self, code=500)
    2     def wrapper(handler):
    3         self.error_handler[int(code)] = handler
    4         return handler
    5     return wrapper

    当程序因找不到合适的视图函数,或者其他内部错误,Bottle._handle()会抛出一个HTTPError,然后在Bottle._cast()中会根据错误状态码在error_handler找到对应的错误处理函数,最后将这个结果当作普通页面来处理

    1 Bottle.wsgi()
    2     out = self._cast(self._handle(environ))
    3 Bottle._cast()
    4     if isinstance(out, HTTPError):
    5         out = self.error_handler.get(out.status_code, self.default_error_handler)(out)
    6     return self._cast(out)

    最后

    Bottle用字典来保存URL映射关系来实现路由和错误页面。现在按照相同的思路,我们来为最简单的WSGI应用添加路由功能和一个简单的错误页面。

     1 class WSGIApp(object):
     2 
     3     def __init__(self):
     4         self.routes = {}
     5 
     6     def route(self, path, method='GET'):
     7         def wrapper(callback):
     8             self.routes.setdefault(method, {})
     9             self.routes[method][path] = callback
    10             return callback
    11         return wrapper
    12 
    13     def error_handler(self, envrion, start_response):
    14         out = [b'Somethind Wrong!']
    15         status = '404 NotFound'
    16         response_headers = [("content-type", "text/plain")]
    17         start_response(status, response_headers)
    18         return out
    19 
    20     def __call__(self, envrion, start_response):
    21         path = envrion['PATH_INFO']
    22         method = envrion['REQUEST_METHOD'].upper()
    23         if method in self.routes and path in self.routes[method]:
    24             handler = self.routes[method][path]
    25         else:
    26             handler = self.error_handler
    27         return handler(envrion, start_response)
    28 
    29 
    30 app = WSGIApp()
    31 
    32 @app.route('/')
    33 def simple_app(envrion, start_response):
    34     out = [b'Hello World!']
    35     status = '200 OK'
    36     response_headers = [("content-type", "text/plain")]
    37     start_response(status, response_headers)
    38     return out
    39 
    40 
    41 if __name__ == '__main__':
    42     from wsgiref.simple_server import make_server
    43     with make_server('', 8000, app) as httpd:
    44         print("Server is Running...")
    45         httpd.serve_forever()
    作者:RainFD
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.
  • 相关阅读:
    Java
    HashMap数据结构与实现原理解析(干货)
    Java
    Java
    Java
    面向对象基础
    Java
    Java
    Java
    shell脚本
  • 原文地址:https://www.cnblogs.com/rainfd/p/bottle2.html
Copyright © 2011-2022 走看看