1. Day 5 - 编写Web框架
    2. 阅读: 84143
    3. 在正式开始Web开发前,我们需要编写一个Web框架。
    4. aiohttp已经是一个Web框架了,为什么我们还需要自己封装一个?
    5. 原因是从使用者的角度来说,aiohttp相对比较底层,编写一个URL的处理函数需要这么几步:
    6. 第一步,编写一个用@asyncio.coroutine装饰的函数:
    7. @asyncio.coroutine
    8. def handle_url_xxx(request):
    9. pass
    10. 第二步,传入的参数需要自己从request中获取:
    11. url_param = request.match_info['key']
    12. query_params = parse_qs(request.query_string)
    13. 最后,需要自己构造Response对象:
    14. text = render('template', data)
    15. return web.Response(text.encode('utf-8'))
    16. 这些重复的工作可以由框架完成。例如,处理带参数的URL/blog/{id}可以这么写:
    17. @get('/blog/{id}')
    18. def get_blog(id):
    19. pass
    20. 处理query_string参数可以通过关键字参数**kw或者命名关键字参数接收:
    21. @get('/api/comments')
    22. def api_comments(*, page='1'):
    23. pass
    24. 对于函数的返回值,不一定是web.Response对象,可以是strbytesdict
    25. 如果希望渲染模板,我们可以这么返回一个dict
    26. return {
    27. '__template__': 'index.html',
    28. 'data': '...'
    29. }
    30. 因此,Web框架的设计是完全从使用者出发,目的是让使用者编写尽可能少的代码。
    31. 编写简单的函数而非引入requestweb.Response还有一个额外的好处,就是可以单独测试,否则,需要模拟一个request才能测试。
    32. @get@post
    33. 要把一个函数映射为一个URL处理函数,我们先定义@get():
    34. def get(path):
    35. '''
    36. Define decorator @get('/path')
    37. '''
    38. def decorator(func):
    39. @functools.wraps(func)
    40. def wrapper(*args, **kw):
    41. return func(*args, **kw)
    42. wrapper.__method__ = 'GET'
    43. wrapper.__route__ = path
    44. return wrapper
    45. return decorator
    46. 这样,一个函数通过@get()的装饰就附带了URL信息。
    47. @post@get定义类似。
    48. 定义RequestHandler
    49. URL处理函数不一定是一个coroutine,因此我们用RequestHandler()来封装一个URL处理函数。
    50. RequestHandler是一个类,由于定义了__call__()方法,因此可以将其实例视为函数。
    51. RequestHandler目的就是从URL函数中分析其需要接收的参数,从request中获取必要的参数,调用URL函数,然后把结果转换为web.Response对象,这样,就完全符合aiohttp框架的要求:
    52. class RequestHandler(object):
    53. def __init__(self, app, fn):
    54. self._app = app
    55. self._func = fn
    56. ...
    57. @asyncio.coroutine
    58. def __call__(self, request):
    59. kw = ... 获取参数
    60. r = yield from self._func(**kw)
    61. return r
    62. 再编写一个add_route函数,用来注册一个URL处理函数:
    63. def add_route(app, fn):
    64. method = getattr(fn, '__method__', None)
    65. path = getattr(fn, '__route__', None)
    66. if path is None or method is None:
    67. raise ValueError('@get or @post not defined in %s.' % str(fn))
    68. if not asyncio.iscoroutinefunction(fn) and not inspect.isgeneratorfunction(fn):
    69. fn = asyncio.coroutine(fn)
    70. logging.info('add route %s %s => %s(%s)' % (method, path, fn.__name__, ', '.join(inspect.signature(fn).parameters.keys())))
    71. app.router.add_route(method, path, RequestHandler(app, fn))
    72. 最后一步,把很多次add_route()注册的调用:
    73. add_route(app, handles.index)
    74. add_route(app, handles.blog)
    75. add_route(app, handles.create_comment)
    76. ...
    77. 变成自动扫描:
    78. # 自动把handler模块的所有符合条件的函数注册了:
    79. add_routes(app, 'handlers')
    80. add_routes()定义如下:
    81. def add_routes(app, module_name):
    82. n = module_name.rfind('.')
    83. if n == (-1):
    84. mod = __import__(module_name, globals(), locals())
    85. else:
    86. name = module_name[n+1:]
    87. mod = getattr(__import__(module_name[:n], globals(), locals(), [name]), name)
    88. for attr in dir(mod):
    89. if attr.startswith('_'):
    90. continue
    91. fn = getattr(mod, attr)
    92. if callable(fn):
    93. method = getattr(fn, '__method__', None)
    94. path = getattr(fn, '__route__', None)
    95. if method and path:
    96. add_route(app, fn)
    97. 最后,在app.py中加入middlewarejinja2模板和自注册的支持:
    98. app = web.Application(loop=loop, middlewares=[
    99. logger_factory, response_factory
    100. ])
    101. init_jinja2(app, filters=dict(datetime=datetime_filter))
    102. add_routes(app, 'handlers')
    103. add_static(app)
    104. middleware
    105. middleware是一种拦截器,一个URL在被某个函数处理前,可以经过一系列的middleware的处理。
    106. 一个middleware可以改变URL的输入、输出,甚至可以决定不继续处理而直接返回。middleware的用处就在于把通用的功能从每个URL处理函数中拿出来,集中放到一个地方。例如,一个记录URL日志的logger可以简单定义如下:
    107. @asyncio.coroutine
    108. def logger_factory(app, handler):
    109. @asyncio.coroutine
    110. def logger(request):
    111. # 记录日志:
    112. logging.info('Request: %s %s' % (request.method, request.path))
    113. # 继续处理请求:
    114. return (yield from handler(request))
    115. return logger
    116. response这个middleware把返回值转换为web.Response对象再返回,以保证满足aiohttp的要求:
    117. @asyncio.coroutine
    118. def response_factory(app, handler):
    119. @asyncio.coroutine
    120. def response(request):
    121. # 结果:
    122. r = yield from handler(request)
    123. if isinstance(r, web.StreamResponse):
    124. return r
    125. if isinstance(r, bytes):
    126. resp = web.Response(body=r)
    127. resp.content_type = 'application/octet-stream'
    128. return resp
    129. if isinstance(r, str):
    130. resp = web.Response(body=r.encode('utf-8'))
    131. resp.content_type = 'text/html;charset=utf-8'
    132. return resp
    133. if isinstance(r, dict):
    134. ...
    135. 有了这些基础设施,我们就可以专注地往handlers模块不断添加URL处理函数了,可以极大地提高开发效率。
    136. 参考源码
    137. day-05