在正式开始Web开发前,我们需要编写一个Web框架。
`aiohttp`已经是一个Web框架了,为什么我们还需要自己封装一个?
原因是从使用者的角度来说,`aiohttp`相对比较底层,编写一个URL的处理函数需要这么几步:
第一步,编写一个用`@asyncio.coroutine`装饰的函数:
~~~
~~~
@asyncio.coroutine
def handle_url_xxx(request):
pass
~~~
~~~
第二步,传入的参数需要自己从`request`中获取:
~~~
~~~
url_param = request.match_info['key']
query_params = parse_qs(request.query_string)
~~~
~~~
最后,需要自己构造`Response`对象:
~~~
~~~
text = render('template', data)
return web.Response(text.encode('utf-8'))
~~~
~~~
这些重复的工作可以由框架完成。例如,处理带参数的URL`/blog/{id}`可以这么写:
~~~
~~~
@get('/blog/{id}')
def get_blog(id):
pass
~~~
~~~
处理`query_string`参数可以通过关键字参数`**kw`或者命名关键字参数接收:
~~~
~~~
@get('/api/comments')
def api_comments(*, page='1'):
pass
~~~
~~~
对于函数的返回值,不一定是`web.Response`对象,可以是`str`、`bytes`或`dict`。
如果希望渲染模板,我们可以这么返回一个`dict`:
~~~
~~~
return {
'__template__': 'index.html',
'data': '...'
}
~~~
~~~
因此,Web框架的设计是完全从使用者出发,目的是让使用者编写尽可能少的代码。
编写简单的函数而非引入`request`和`web.Response`还有一个额外的好处,就是可以单独测试,否则,需要模拟一个`request`才能测试。
### @get和@post
要把一个函数映射为一个URL处理函数,我们先定义`@get()`:
~~~
~~~
def get(path):
'''
Define decorator @get('/path')
'''
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
return func(*args, **kw)
wrapper.__method__ = 'GET'
wrapper.__route__ = path
return wrapper
return decorator
~~~
~~~
这样,一个函数通过`@get()`的装饰就附带了URL信息。
`@post`与`@get`定义类似。
### 定义RequestHandler
URL处理函数不一定是一个`coroutine`,因此我们用`RequestHandler()`来封装一个URL处理函数。
`RequestHandler`是一个类,由于定义了`__call__()`方法,因此可以将其实例视为函数。
`RequestHandler`目的就是从URL函数中分析其需要接收的参数,从`request`中获取必要的参数,调用URL函数,然后把结果转换为`web.Response`对象,这样,就完全符合`aiohttp`框架的要求:
~~~
~~~
class RequestHandler(object):
def __init__(self, app, fn):
self._app = app
self._func = fn
...
@asyncio.coroutine
def __call__(self, request):
kw = ... 获取参数
r = yield from self._func(**kw)
return r
~~~
~~~
再编写一个`add_route`函数,用来注册一个URL处理函数:
~~~
~~~
def add_route(app, fn):
method = getattr(fn, '__method__', None)
path = getattr(fn, '__route__', None)
if path is None or method is None:
raise ValueError('@get or @post not defined in %s.' % str(fn))
if not asyncio.iscoroutinefunction(fn) and not inspect.isgeneratorfunction(fn):
fn = asyncio.coroutine(fn)
logging.info('add route %s %s => %s(%s)' % (method, path, fn.__name__, ', '.join(inspect.signature(fn).parameters.keys())))
app.router.add_route(method, path, RequestHandler(app, fn))
~~~
~~~
最后一步,把很多次`add_route()`注册的调用:
~~~
~~~
add_route(app, handles.index)
add_route(app, handles.blog)
add_route(app, handles.create_comment)
...
~~~
~~~
变成自动扫描:
~~~
~~~
# 自动把handler模块的所有符合条件的函数注册了:
add_routes(app, 'handlers')
~~~
~~~
`add_routes()`定义如下:
~~~
~~~
def add_routes(app, module_name):
n = module_name.rfind('.')
if n == (-1):
mod = __import__(module_name, globals(), locals())
else:
name = module_name[n+1:]
mod = getattr(__import__(module_name[:n], globals(), locals(), [name]), name)
for attr in dir(mod):
if attr.startswith('_'):
continue
fn = getattr(mod, attr)
if callable(fn):
method = getattr(fn, '__method__', None)
path = getattr(fn, '__route__', None)
if method and path:
add_route(app, fn)
~~~
~~~
最后,在`app.py`中加入`middleware`、`jinja2`模板和自注册的支持:
~~~
~~~
app = web.Application(loop=loop, middlewares=[
logger_factory, response_factory
])
init_jinja2(app, filters=dict(datetime=datetime_filter))
add_routes(app, 'handlers')
add_static(app)
~~~
~~~
### middleware
`middleware`是一种拦截器,一个URL在被某个函数处理前,可以经过一系列的`middleware`的处理。
一个`middleware`可以改变URL的输入、输出,甚至可以决定不继续处理而直接返回。middleware的用处就在于把通用的功能从每个URL处理函数中拿出来,集中放到一个地方。例如,一个记录URL日志的`logger`可以简单定义如下:
~~~
~~~
@asyncio.coroutine
def logger_factory(app, handler):
@asyncio.coroutine
def logger(request):
# 记录日志:
logging.info('Request: %s %s' % (request.method, request.path))
# 继续处理请求:
return (yield from handler(request))
return logger
~~~
~~~
而`response`这个`middleware`把返回值转换为`web.Response`对象再返回,以保证满足`aiohttp`的要求:
~~~
~~~
@asyncio.coroutine
def response_factory(app, handler):
@asyncio.coroutine
def response(request):
# 结果:
r = yield from handler(request)
if isinstance(r, web.StreamResponse):
return r
if isinstance(r, bytes):
resp = web.Response(body=r)
resp.content_type = 'application/octet-stream'
return resp
if isinstance(r, str):
resp = web.Response(body=r.encode('utf-8'))
resp.content_type = 'text/html;charset=utf-8'
return resp
if isinstance(r, dict):
...
~~~
~~~
有了这些基础设施,我们就可以专注地往`handlers`模块不断添加URL处理函数了,可以极大地提高开发效率。
### 参考源码
[day-05](https://github.com/michaelliao/awesome-python3-webapp/tree/day-05)
- 关于
- Python简介
- 安装Python
- Python解释器
- 第一个Python程序
- 使用文本编辑器
- Python代码运行助手
- 输入和输出
- Python基础
- 数据类型和变量
- 字符串和编码
- 使用list和tuple
- 条件判断
- 循环
- 使用dict和set
- 函数
- 调用函数
- 定义函数
- 函数的参数
- 递归函数
- 高级特性
- 切片
- 迭代
- 列表生成式
- 生成器
- 迭代器
- 函数式编程
- 高阶函数
- 返回函数
- 匿名函数
- 装饰器
- 偏函数
- 模块
- 使用模块
- 安装第三方模块
- 面向对象编程
- 类和实例
- 访问限制
- 继承和多态
- 获取对象信息
- 实例属性和类属性
- 面向对象高级编程
- 使用slots
- 使用@property
- 多重继承
- 定制类
- 使用枚举类
- 使用元类
- 错误、调试和测试
- 错误处理
- 调试
- 单元测试
- 文档测试
- IO编程
- 文件读写
- StringIO和BytesIO
- 操作文件和目录
- 序列化
- 进程和线程
- 多进程
- 多线程
- ThreadLocal
- 进程 vs. 线程
- 分布式进程
- 正则表达式
- 常用内建模块
- datetime
- collections
- base64
- struct
- hashlib
- itertools
- XML
- HTMLParser
- urllib
- 常用第三方模块
- PIL
- virtualenv
- 图形界面
- 网络编程
- TCP/IP简介
- TCP编程
- UDP编程
- 电子邮件
- SMTP发送邮件
- POP3收取邮件
- 访问数据库
- 使用SQLite
- 使用MySQL
- 使用SQLAlchemy
- Web开发
- HTTP协议简介
- HTML简介
- WSGI接口
- 使用Web框架
- 使用模板
- 异步IO
- 协程
- asyncio
- aiohttp
- 实战
- Day 1 - 搭建开发环境
- Day 2 - 编写Web App骨架
- Day 3 - 编写ORM
- Day 4 - 编写Model
- Day 5 - 编写Web框架
- Day 6 - 编写配置文件
- Day 7 - 编写MVC
- Day 8 - 构建前端
- Day 9 - 编写API
- Day 10 - 用户注册和登录
- Day 11 - 编写日志创建页
- Day 12 - 编写日志列表页
- Day 13 - 提升开发效率
- Day 14 - 完成Web App
- Day 15 - 部署Web App
- Day 16 - 编写移动App
- FAQ
- 期末总结
- CoverPage