aiohttp是一个较底层的框架,当有HTTP请求进入,aiohttp会生成一个
request
对象,经处理后返回一个
Response
对象。
但是,中间的处理过程需要我们自行去完成,所以我们要
在aiohttp基础上自己封装一个框架
。
1. 编写视图函数(URL处理函数)
1.1 aiohttp编写视图函数
如果仅仅用aiohttp编写视图函数,我们需要以下几步:
第一步,编写一个由
async/await
装饰的函数:
- async def handle_url_xxx(request):
- ...
第二步,传入视图函数的参数要自己从
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'))
以上这些重复的过程,
可以在新建的框架中封装起来。
1.2 新建框架编写视图函数
首先
编写一个构造视图函数的装饰器
,其中传递、储存URL信息
(path,method)
。
- # 建立视图函数装饰器,用来存储、附带URL信息
- def Handler_decorator(path, *, method):
- def decorator(func):
- @functools.wraps(func)
- def warpper(*args, **kw):
- return func(*args, **kw)
- warpper.__route__ = path
- warpper.__method__ = method
- return warpper
- return decorator
- # 偏函数。GET POST 方法的路由装饰器
- get = functools.partial(Handler_decorator, method = 'GET')
- post = functools.partial(Handler_decorator, method = 'POST')
这样就可以
直接通过装饰器,直接将一个函数映射成视图函数。
- @get
- def View(request):
- return response
1.3 编写RequestHandler:处理request对象
我们已经完成了视图函数装饰器的编写,但这远远不够。视图函数仍无法从
request
中获取参数。
所以我们还要从
request
对象中提取相应视图函数所需的参数,并且视图函数并非都是
coroutine
。
因此,
需要定义一个能处理
request
请求的类来对视图函数进行封装,
RequestHandler
。
RequestHandler
是一个类,分析视图函数所需的参数,再从
request
对象中将参数提取,调用视图函数(URL处理函数),并返回
web.Response
对象。
由于其定义了
__call__()
方法,其实例对象可以看作函数。
用这样一个
RequestHandler
类,就能处理各类
request
向对应视图函数发起的请求了。
1.3.1 解析视图函数
使用python自带库的
inspect
模块,
解析视图函数的参数
。
参考资料:inspect
- # 使用inspect模块,检查视图函数的参数
- # inspect.Parameter.kind 类型:
- # POSITIONAL_ONLY 位置参数
- # KEYWORD_ONLY 命名关键词参数
- # VAR_POSITIONAL 可选参数 *args
- # VAR_KEYWORD 关键词参数 **kw
- # POSITIONAL_OR_KEYWORD 位置或必选参数
- def get_required_kw_args(fn): # 获取无默认值的命名关键词参数
- args = []
- '''''
- def foo(a, b = 10, *c, d,**kw): pass
- sig = inspect.signature(foo) ==> <Signature (a, b=10, *c, d, **kw)>
- sig.parameters ==> mappingproxy(OrderedDict([('a', <Parameter "a">), ...]))
- sig.parameters.items() ==> odict_items([('a', <Parameter "a">), ...)])
- sig.parameters.values() ==> odict_values([<Parameter "a">, ...])
- sig.parameters.keys() ==> odict_keys(['a', 'b', 'c', 'd', 'kw'])
- '''
- params = inspect.signature(fn).parameters
- for name, param in params.items():
- # 如果视图函数存在命名关键字参数,且默认值为空,获取它的key(参数名)
- if param.kind == inspect.Parameter.KEYWORD_ONLY and param.default == inspect.Parameter.empty:
- args.append(name)
- return tuple(args)
- def get_named_kw_args(fn): # 获取命名关键词参数
- args = []
- params = inspect.signature(fn).parameters
- for name, param in params.items():
- if param.kind == inspect.Parameter.KEYWORD_ONLY:
- args.append(name)
- return tuple(args)
- def has_named_kw_arg(fn): # 判断是否有命名关键词参数
- params = inspect.signature(fn).parameters
- for name, param in params.items():
- if param.kind == inspect.Parameter.KEYWORD_ONLY:
- return True
- def has_var_kw_arg(fn): # 判断是否有关键词参数
- params = inspect.signature(fn).parameters
- for name, param in params.items():
- if param.kind == inspect.Parameter.VAR_KEYWORD:
- return True
- def has_request_arg(fn): # 判断是否含有名叫'request'的参数,且位置在最后
- params = inspect.signature(fn).parameters
- found = False
- for name, param in params.items():
- if name == 'request':
- found = True
- continue
- if found and (
- param.kind != inspect.Parameter.VAR_POSITIONAL and
- param.kind != inspect.Parameter.KEYWORD_ONLY and
- param.kind != inspect.Parameter.VAR_KEYWORD):
- # 若判断为True,表明param只能是位置参数。且该参数位于request之后,故不满足条件,报错。
- raise ValueError('request parameter must be the last named parameter in function:%s%s' % (fn.__name__, str(sig)))
- return found
1.3.2 提取request中的参数
request
是经aiohttp包装后的对象。其本质是一个
HTTP请求
。
由
请求状态(status)
、
请求首部(header)
、
内容实体(body)
三部分组成。
我们需要的参数包含在内容实体以及请求状态URI中。
request
对象封装了HTTP请求,可以通过
request
的属性调取值。
参考资料:aiohttp.web.Request
RequestHandler
需要处理以下问题:
1、确定HTTP请求的方法(’
POST’or’GET
’)(用
request.method
获取)
2、根据HTTP请求的
content_type
字段,选用不同解析方法获取参数。(用
request.content_type
获取)
3、将获取的参数经处理,使其完全符合视图函数接收的参数形式
4、调用视图函数
- # 定义RequestHandler从视图函数中分析其需要接受的参数,从web.Request中获取必要的参数
- # 调用视图函数,然后把结果转换为web.Response对象,符合aiohttp框架要求
- class RequestHandler(object):
- def __init__(self, app, fn):
- self._app = app
- self._func = fn
- self._required_kw_args = get_required_kw_args(fn)
- self._named_kw_args = get_named_kw_args(fn)
- self._has_request_arg = has_request_arg(fn)
- self._has_named_kw_arg = has_named_kw_arg(fn)
- self._has_var_kw_arg = has_var_kw_arg(fn)
- # 1.定义kw,用于保存参数
- # 2.判断视图函数是否存在关键词参数,如果存在根据POST或者GET方法将request请求内容保存到kw
- # 3.如果kw为空(说明request无请求内容),则将match_info列表里的资源映射给kw;若不为空,把命名关键词参数内容给kw
- # 4.完善_has_request_arg和_required_kw_args属性
- async def __call__(self, request):
- kw = None # 定义kw,用于保存request中参数
- if self._has_named_kw_arg or self._has_var_kw_arg: # 若视图函数有命名关键词或关键词参数
- if request.method == 'POST':
- # 根据request参数中的content_type使用不同解析方法:
- if request.content_type == None: # 如果content_type不存在,返回400错误
- return web.HTTPBadRequest(text='Missing Content_Type.')
- ct = request.content_type.lower() # 小写,便于检查
- if ct.startwith('application/json'): # json格式数据
- params = await request.json() # 仅解析body字段的json数据
- if not isinstance(params, dict): # request.json()返回dict对象
- return web.HTTPBadRequest(text='JSON body must be object.')
- kw = params
- # form表单请求的编码形式
- elif ct.startwith('application/x-www-form-urlencoded') or ct.startswith('multipart/form-data'):
- params = await request.post() # 返回post的内容中解析后的数据。dict-like对象。
- kw = dict(**params) # 组成dict,统一kw格式
- else:
- return web.HTTPBadRequest(text='Unsupported Content-Type: %s' % request.content_type)
- if request.method == 'GET':
- qs = request.query_string # 返回URL查询语句,?后的键值。string形式。
- if qs:
- kw = dict()
- '''''
- 解析url中?后面的键值对的内容
- qs = 'first=f,s&second=s'
- parse.parse_qs(qs, True).items()
- >>> dict([('first', ['f,s']), ('second', ['s'])])
- '''
- for k, v in parse.parse_qs(qs, True).items(): # 返回查询变量和值的映射,dict对象。True表示不忽略空格。
- kw[k] = v[0]
- if kw is None: # 若request中无参数
- # request.match_info返回dict对象。可变路由中的可变字段{variable}为参数名,传入request请求的path为值
- # 若存在可变路由:/a/{name}/c,可匹配path为:/a/jack/c的request
- # 则reqwuest.match_info返回{name = jack}
- kw = dict(**request.match_info)
- else: # request有参数
- if self._has_named_kw_arg and (not self._has_var_kw_arg): # 若视图函数只有命名关键词参数没有关键词参数
- copy = dict()
- # 只保留命名关键词参数
- for name in self._named_kw_arg:
- if name in kw:
- copy[name] = kw[name]
- kw = copy # kw中只存在命名关键词参数
- # 将request.match_info中的参数传入kw
- for k, v in request.match_info.items():
- # 检查kw中的参数是否和match_info中的重复
- if k in kw:
- logging.warn('Duplicate arg name in named arg and kw args: %s' % k)
- kw[k] = v
- if self._has_request_arg: # 视图函数存在request参数
- kw['request'] = request
- if self._required_kw_args: # 视图函数存在无默认值的命名关键词参数
- for name in self._required_kw_args:
- if not name in kw: # 若未传入必须参数值,报错。
- return web.HTTPBadRequest('Missing argument: %s' % name)
- # 至此,kw为视图函数fn真正能调用的参数
- # request请求中的参数,终于传递给了视图函数
- logging.info('call with args: %s' % str(kw))
- #try:
- r = await self._func(**kw)
- return r
- #except APIerror as e:
- #return dict(error=e.error, data=e.data, message=e.message)
2. 编写add_route函数及add_static函数
2.1 编写视图函数注册函数
完成了
RequestHandler
类的编写,我们需要在
app
中
注册视图函数
(添加路由)。
add_route
函数功能:
1、验证视图函数是否拥有
method
和
path
参数
2、将视图函数转变为协程
参考资料:
- # 编写一个add_route函数,用来注册一个视图函数
- def add_route(app, fn):
- method = getattr(fn, '__method__', None)
- path = getattr(fn, '__route__', None)
- if method is None or path is None:
- raise ValueError('@get or @post not defined in %s.' % fn.__name__)
- # 判断URL处理函数是否协程并且是生成器
- if not asyncio.iscoroutinefunction(fn) and not inspect.isgeneratorfunction(fn):
- # 将fn转变成协程
- fn = asyncio.coroutine(fn)
- logging.info('add route %s %s => %s(%s)' % (method, path, fn.__name__, ','.join(inspect.signature(fn).parameters.keys())))
- # 在app中注册经RequestHandler类封装的视图函数
- app.router.add_route(method, path, RequestHandler(app, fn))
add_route
函数每次只能注册一个视图函数。
若要
批量注册视图函
数,需要编写一个批注册函数
add_routes
。
希望只提供模块路径,批注册函数将自动导入其中的视图函数进行注册。
- # 导入模块,批量注册视图函数
- def add_routes(app, module_name):
- n = module_name.rfind('.') # 从右侧检索,返回索引。若无,返回-1。
- # 导入整个模块
- if n == -1:
- # __import__ 作用同import语句,但__import__是一个函数,并且只接收字符串作为参数
- # __import__('os',globals(),locals(),['path','pip'], 0) ,等价于from os import path, pip
- mod = __import__(module_name, globals(), locals, [], 0)
- else:
- name = module_name[(n+1):]
- # 只获取最终导入的模块,为后续调用dir()
- mod = getattr(__import__(module_name[:n], globals(), locals, [name], 0), name)
- for attr in dir(mod): # dir()迭代出mod模块中所有的类,实例及函数等对象,str形式
- if attr.startswith('_'):
- continue # 忽略'_'开头的对象,直接继续for循环
- fn = getattr(mod, attr)
- # 确保是函数
- if callable(fn):
- # 确保视图函数存在method和path
- method = getattr(fn, '__method__', None)
- path = getattr(fn, '__route__', None)
- if method and path:
- # 注册
- add_route(app, fn)
2.2 编写静态文件注册函数
编写
add_static
函数用于
注册静态文件
,只提供文件路径即可进行注册
- # 添加静态文件,如image,css,javascript等
- def add_static(app):
- # 拼接static文件目录
- path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
- # path = os.path.join(os.path.abspath('.'), 'static')
- app.router.add_static('/static/', path)
- logging.info('add static %s => %s' % ('/static/', path))
3. 初始化jinja2模板
我们使用jinja2作为模板引擎,在新框架中对jinja2模板进行初始化设置。
参考资料:jinja2
初始化jinja2需要以下几步:
1、对Environment类的参数
options
进行配置。
2、使用jinja提供的模板加载器加载模板文件,程序中选用
FileSystemLoader
加载器直接从模板文件夹加载模板。
3、有了加载器和
options
参数,传递给
Environment
类,添加过滤器,完成初始化。
- def init_jinja2(app, **kw):
- logging.info('init jinja2...')
- # class Environment(**options)
- # 配置options参数
- options = dict(
- # 自动转义xml/html的特殊字符
- autoescape = kw.get('autoescape', True),
- # 代码块的开始、结束标志
- block_start_string = kw.get('block_start_string', '{%'),
- block_end_string = kw.get('block_end_string', '%}'),
- # 变量的开始、结束标志
- variable_start_string = kw.get('variable_start_string', '{{'),
- variable_end_string = kw.get('variable_end_string', '}}'),
- # 自动加载修改后的模板文件
- auto_reload = kw.get('auto_reload', True)
- )
- # 获取模板文件夹路径
- path = kw.get('path', None)
- if not path:
- path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates')
- # Environment类是jinja2的核心类,用来保存配置、全局对象以及模板文件的路径
- # FileSystemLoader类加载path路径中的模板文件
- env = Environment(loader = FileSystemLoader(path), **options)
- # 过滤器集合
- filters = kw.get('filters', None)
- if filters:
- for name, f in filters.items():
- # filters是Environment类的属性:过滤器字典
- env.filters[name] = f
- # 所有的一切是为了给app添加__templating__字段
- # 前面将jinja2的环境配置都赋值给env了,这里再把env存入app的dict中,这样app就知道要到哪儿去找模板,怎么解析模板。
- app['__template__'] = env # app是一个dict-like对象
编写一个过滤器:
- def datetime_filter(t):
- delta = int(time.time() - t)
- if delta < 60:
- return u'1分钟前'
- if delta < 3600:
- return u'%s分钟前' % (delta//60)
- if delta < 86400:
- return u'%s小时前' % (delta//3600)
- if delta < 604800:
- return u'%s天前' % (delta//86400)
- dt = datetime.fromtimestamp(t)
- return u'%s年%s月%s日' % (dt.year, dt.month, dt.day)
4. 编写middleware
middlerware是符合WSGI定义的中间件
。位于
服务端
和
客户端
之间对数据进行拦截处理的一个桥梁。可以看过服务器端的数据,经
middleware
一层层封装,最终传递给客户端。
参考资料:WSGI
web框架正是由一层层middleware的封装,才具备各种完善的功能。所以我们
需要编写middlerware对视图函数返回的数据进行处理
。如:构造
Response
对象。
其实middleware和装饰器类似,它会对视图函数返回的数据进行处理,达成想要的目的。
编写一个简单的用于打印日志的
middleware:
# 编写用于输出日志的middleware
- # 编写用于输出日志的middleware
- # handler是视图函数
- async def logger_factory(app, handler):
- async def logger(request):
- logging.info('Request: %s %s' % (request.method, request.path))
- return await handler(request)
- return logger
接下来,编写构造
Response对象的
middleware:
- # 处理视图函数返回值,制作response的middleware
- # 请求对象request的处理工序:
- # logger_factory => response_factory => RequestHandler().__call__ => handler
- # 响应对象response的处理工序:
- # 1、由视图函数处理request后返回数据
- # 2、@get@post装饰器在返回对象上附加'__method__'和'__route__'属性,使其附带URL信息
- # 3、response_factory对处理后的对象,经过一系列类型判断,构造出真正的web.Response对象
- async def response_factory(app, handler):
- async def response(request):
- logging.info('Response handler...')
- r = await handler(request)
- logging.info('response result = %s' % str(r))
- if isinstance(r, web.StreamResponse): # StreamResponse是所有Response对象的父类
- return r # 无需构造,直接返回
- if isinstance(r, bytes):
- logging.info('*'*10)
- resp = web.Response(body=r) # 继承自StreamResponse,接受body参数,构造HTTP响应内容
- # Response的content_type属性
- resp.content_type = 'application/octet-stream'
- return resp
- if isinstance(r, str):
- if r.startswith('redirect:'): # 若返回重定向字符串
- return web.HTTPFound(r[9:]) # 重定向至目标URL
- resp = web.Response(body=r.encode('utf-8'))
- resp.content_type = 'text/html;charset=utf-8' # utf-8编码的text格式
- return resp
- # r为dict对象时
- if isinstance(r, dict):
- # 在后续构造视图函数返回值时,会加入__template__值,用以选择渲染的模板
- template = r.get('__template__', None)
- if template is None: # 不带模板信息,返回json对象
- resp = web.Response(body=json.dumps(r, ensure_ascii=False, default=lambda obj: obj.__dict__).encode('utf-8'))
- # ensure_ascii:默认True,仅能输出ascii格式数据。故设置为False。
- # default:r对象会先被传入default中的函数进行处理,然后才被序列化为json对象
- # __dict__:以dict形式返回对象属性和值的映射
- resp.content_type = 'application/json;charset=utf-8'
- return resp
- else: # 带模板信息,渲染模板
- # app['__templating__']获取已初始化的Environment对象,调用get_template()方法返回Template对象
- # 调用Template对象的render()方法,传入r渲染模板,返回unicode格式字符串,将其用utf-8编码
- resp = web.Response(body=app['__template__'].get_template(template).render(**r))
- resp.content_type = 'text/html;charset=utf-8' # utf-8编码的html格式
- return resp
- # 返回响应码
- if isinstance(r, int) and (600>r>=100):
- resp = web.Response(status=r)
- return resp
- # 返回了一组响应代码和原因,如:(200, 'OK'), (404, 'Not Found')
- if isinstance(r, tuple) and len(r) == 2:
- status_code, message = r
- if isinstance(status_code, int) and (600>status_code>=100):
- resp = web.Response(status=r, text=str(message))
- resp = web.Response(body=str(r).encode('utf-8')) # 均以上条件不满足,默认返回
- resp.content_type = 'text/plain;charset=utf-8' # utf-8纯文本
- return resp
- return response
5. 测试
创建一个test_view.py文件在同一目录下,用来编写测试用的视图函数:
- from coroweb import get
- import asyncio
- @get('/')
- async def index(request):
- return '<h1>Awesome</h1>'
- @get('/hello')
- async def hello(request):
- return '<h1>hello!</h1>'
初始化app实例,运行:
- if __name__ == '__main__':
- async def init(loop):
- app = web.Application(loop = loop, middlewares=[logger_factory, response_factory])
- init_jinja2(app, filters=dict(datetime = datetime_filter))
- add_routes(app, 'test_view')
- add_static(app)
- srv = await loop.create_server(app.make_handler(), 'localhost', 9000)
- logging.info('server started at http://127.0.0.1:9000...')
- return srv
- loop = asyncio.get_event_loop()
- loop.run_until_complete(init(loop))
- loop.run_forever()
用浏览器访问地址:localhost:9000 ~