Python实战网站开发:Day7-搭建MVC
编写MVC
MVC模式(Model-View-Controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。现在我们的ORM框架、Web框架和配置都已就绪,编写一个简单的MVC,就可以把它们全部启动起来。
通过Web框架的@get
和ORM框架的Model支持,可以很容易地编写一个处理首页URL的函数(新建handlers.py进行编写):
from models import User from coroweb import get import asyncio @get('/') async def index(request): users = await User.findAll() return { '__template__': 'test.html', 'users': users }
'__template__'
指定的模板文件是test.html
,其他参数是传递给模板的数据,所以我们在模板的根目录templates
下创建test.html
:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Test users - Awesome Python Webapp</title> </head> <body> <h1>All users</h1> {% for u in users %} <p>{{ u.name }} / {{ u.email }}</p> {% endfor %} </body> </html>
接下来,如果一切顺利,可以用命令行启动Web服务器:
$ python3 app.py
然后,在浏览器中访问http://localhost:9000/
。
如果数据库的users
表什么内容也没有,你就无法在浏览器中看到循环输出的内容。可以自己在MySQL的命令行里给users
表添加几条记录,或者使用ORM那一章节里使用过的test.py添加users,然后再访问。
数据库已经有一条以后信息数据,所以我们的页面会显示如下,如果有多个则会一一列出
看到这里参考的博主写到这里,按照博主步骤依葫芦画瓢是可以正常运行的,但是完全不知道主程序app.py的运行过程,要想了解app.py的运行过程,有必要拆分解析app.py代码中的初始化函数init运行步骤
查看文件app.p的yinit部分代码
async def init(loop): await orm.create_pool(loop=loop, **configs.db) ## 在handlers.py完全完成后,在下面middlewares的list中加入auth_factory app = web.Application(middlewares=[ logger_factory, response_factory ]) init_jinja2(app, filters=dict(datetime=datetime_filter)) add_routes(app, 'handlers') add_static(app) runner = web.AppRunner(app) await runner.setup() srv = web.TCPSite(runner, '0.0.0.0', 9000) logging.info('fn is "init" server started at http://127.0.0.1:9000...') await srv.start() if __name__ == '__main__': loop = asyncio.get_event_loop() loop.run_until_complete(init(loop)) loop.run_forever()
1,创建数据库连接池
创建数据库连接池
await orm.create_pool(loop=loop, **configs.db)
创建数据库连接池在第3天的文章中又详细解析,这里不重复参考:https://www.cnblogs.com/minseo/p/15538636.html
其中数据库的配置文件为config_default.py,从配置文件获得的到一个字典**configs.db传递给创建数据库连接的的协程函数create_pool
# config_default.py configs = { 'debug': True, 'db': { 'host': 'localhost', 'port': 3306, 'user': 'www-data', 'password': 'www-data', 'db': 'awesome' }, 'session': { 'secret': 'Awesome' } }
2,创建app并设置拦截
app = web.Application(middlewares=[ logger_factory, response_factory ])
只知道是用于某些权限的页面拦截,例如非管理员不能查看管理员权限的页面,还没看懂暂时省略...
3,初始化jinjia2模板
init_jinja2(app, filters=dict(datetime=datetime_filter))
这里把app和一个字典的键值对代入初始化函数init_jinja2进行初始化,针对jinja的默认变量进行修改,然后把修改以后的默认env赋值给app的属性'__templating__'
下面以代码说明app进过这个初始化函数以后有哪些改变
C:/Python37/python.exe d:/learn-python3/实战/awesome-python3-webapp/www/init_jinja2_split.py
import logging; logging.basicConfig(level=logging.INFO) from datetime import datetime from aiohttp import web import os from jinja2 import Environment, FileSystemLoader def init_jinja2(app, **kw): logging.info('init jinja2...') # 定义一个字典,用于修改jinja2默认的Environment # 这里实际修改了autoescape,默认为False,修改为True options = dict( 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) ) print(options) # 从传递的字典kw中获取path属性 # 本次没有传递,使用为空 path = kw.get('path', None) # path是空的所以执行了if下的语句 # 其中os.path.abspath(__file__)获取当前执行文件的绝对路径 本次为 d:\learn-python3\实战\awesome-python3-webapp\www\init_jinja2_split.py # os.path.dirname(os.path.abspath(__file__))通过绝对路径获取到文件夹 本次为 d:\learn-python3\实战\awesome-python3-webapp\www\ # os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates')把文件夹和'templates'拼接 本次为 d:\learn-python3\实战\awesome-python3-webapp\www\templates # 即相当于执行以下代码获得了当前执行的py文件的文件夹下的文件夹templates完整路径 if path is None: path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates') logging.info('set jinja2 template path: %s' % path) # 修改默认的Environment()默认为一个字典存储了各种键值对,以键值对或者字典的方式传递 # FileSystemLoader为文件系统加载器,不需要模板文件存在某个Python包下,可以直接访问系统中的文件 # 如果默认的Environment()没有传递的键值对则添加,如果有则修改 # 例如默认的有key为'autoescape'对应的value为False options字典有这个key但是值为True所以修改了这个属性 env = Environment(loader=FileSystemLoader(path), **options) #print(env.__dict__) print(kw) # 从传递的字典中去查找属性filters,本次是有传递的不为空对应的值是一个字典{'datetime':datetime_filter} filters = kw.get('filters', None) # 不为空,所以执行if语句,把这个字典的key,value添加至默认env的属性'filter'中 # 默认的属性'filter'值为一个字典,字典的key,value为方法名和对应的一个函数 if filters is not None: for name, f in filters.items(): env.filters[name] = f # 把修改后的env赋值给app的属性'__templating__' app['__templating__'] = env # 时间转换函数,传递一个时间戳计算与当前时间的相差时间,如果相差查过小于60秒则返回1分钟前,以此类推,如果相差查过365天则返回确切的时间戳对应的时间 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) # 通过web生成一个app app = web.Application() # 把app和filters=dict(datetime=datetime_filter)作为参数传递给初始化函数init_jinja2 init_jinja2(app, filters=dict(datetime=datetime_filter)) # 为了对比dict1存储了默认的Enviroment().__dict__ # dict1存储了进过初始化修改以后的__dict__ dict1 = Environment().__dict__ dict2 = app['__templating__'].__dict__ # 为了便于对比使用key,value的方法打印 for k,v in dict1.items(): print(k,v) for k,v in dict2.items(): print(k,v)
输出如下
INFO:root:init jinja2... {'autoescape': True, 'block_start_string': '{%', 'block_end_string': '%}', 'variable_start_string': '{{', 'variable_end_string': '}}', 'auto_reload': True} INFO:root:set jinja2 template path: d:\learn-python3\实战\awesome-python3-webapp\www\templates {'filters': {'datetime': <function datetime_filter at 0x0000022FA96344C8>}} block_start_string {% block_end_string %} variable_start_string {{ variable_end_string }} comment_start_string {# comment_end_string #} line_statement_prefix None line_comment_prefix None trim_blocks False lstrip_blocks False newline_sequence keep_trailing_newline False undefined <class 'jinja2.runtime.Undefined'> optimized True finalize None autoescape False filters {'abs': <built-in function abs>, 'attr': <function do_attr at 0x0000022FA9756798>, 'batch': <function do_batch at 0x0000022FA9752678>, 'capitalize': <function do_capitalize at 0x0000022FA97401F8>, 'center': <function do_center at 0x0000022FA974C948>, 'count': <built-in function len>, 'd': <function do_default at 0x0000022FA974C828>, 'default': <function do_default at 0x0000022FA974C828>, 'dictsort': <function do_dictsort at 0x0000022FA974C4C8>, 'e': <built-in function escape>, 'escape': <built-in function escape>, 'filesizeformat': <function do_filesizeformat at 0x0000022FA974CEE8>, 'first': <function sync_do_first at 0x0000022FA974CDC8>, 'float': <function do_float at 0x0000022FA97523A8>, 'forceescape': <function do_forceescape at 0x0000022FA9736D38>, 'format': <function do_format at 0x0000022FA9752438>, 'groupby': <function sync_do_groupby at 0x0000022FA9756168>, 'indent': <function do_indent at 0x0000022FA97520D8>, 'int': <function do_int at 0x0000022FA9752318>, 'join': <function sync_do_join at 0x0000022FA974CAF8>, 'last': <function do_last at 0x0000022FA974CC18>, 'length': <built-in function len>, 'list': <function sync_do_list at 0x0000022FA97565E8>, 'lower': <function do_lower at 0x0000022FA9740048>, 'map': <function sync_do_map at 0x0000022FA9756A68>, 'min': <function do_min at 0x0000022FA974C708>, 'max': <function do_max at 0x0000022FA974C798>, 'pprint': <function do_pprint at 0x0000022FA974CF78>, 'random': <function do_random at 0x0000022FA974CE58>, 'reject': <function sync_do_reject at 0x0000022FA9756EE8>, 'rejectattr': <function sync_do_rejectattr at 0x0000022FA975A3A8>, 'replace': <function do_replace at 0x0000022FA973AA68>, 'reverse': <function do_reverse at 0x0000022FA9756708>, 'round': <function do_round at 0x0000022FA97528B8>, 'safe': <function do_mark_safe at 0x0000022FA9756438>, 'select': <function sync_do_select at 0x0000022FA9756CA8>, 'selectattr': <function sync_do_selectattr at 0x0000022FA975A168>, 'slice': <function sync_do_slice at 0x0000022FA9752828>, 'sort': <function do_sort at 0x0000022FA974C558>, 'string': <built-in function soft_str>, 'striptags': <function do_striptags at 0x0000022FA9752558>, 'sum': <function sync_do_sum at 0x0000022FA97563A8>, 'title': <function do_title at 0x0000022FA974C288>, 'trim': <function do_trim at 0x0000022FA97524C8>, 'truncate': <function do_truncate at 0x0000022FA9752168>, 'unique': <function do_unique at 0x0000022FA974C5E8>, 'upper': <function do_upper at 0x0000022FA9740288>, 'urlencode': <function do_urlencode at 0x0000022FA973A1F8>, 'urlize': <function do_urlize at 0x0000022FA9752048>, 'wordcount': <function do_wordcount at 0x0000022FA9752288>, 'wordwrap': <function do_wordwrap at 0x0000022FA97521F8>, 'xmlattr': <function do_xmlattr at 0x0000022FA9740168>, 'tojson': <function do_tojson at 0x0000022FA975A1F8>} tests {'odd': <function test_odd at 0x0000022FA966B1F8>, 'even': <function test_even at 0x0000022FA975D0D8>, 'divisibleby': <function test_divisibleby at 0x0000022FA975DD38>, 'defined': <function test_defined at 0x0000022FA97618B8>, 'undefined': <function test_undefined at 0x0000022FA9761AF8>, 'filter': <function test_filter at 0x0000022FA97645E8>, 'test': <function test_test at 0x0000022FA9764558>, 'none': <function test_none at 0x0000022FA966F4C8>, 'boolean': <function test_boolean at 0x0000022FA976B678>, 'false': <function test_false at 0x0000022FA976B708>, 'true': <function test_true at 0x0000022FA976B798>, 'integer': <function test_integer at 0x0000022FA976B828>, 'float': <function test_float at 0x0000022FA976B8B8>, 'lower': <function test_lower at 0x0000022FA976B948>, 'upper': <function test_upper at 0x0000022FA976B9D8>, 'string': <function test_string at 0x0000022FA976BA68>, 'mapping': <function test_mapping at 0x0000022FA976BAF8>, 'number': <function test_number at 0x0000022FA976BB88>, 'sequence': <function test_sequence at 0x0000022FA976BC18>, 'iterable': <function test_iterable at 0x0000022FA976BD38>, 'callable': <built-in function callable>, 'sameas': <function test_sameas at 0x0000022FA976BCA8>, 'escaped': <function test_escaped at 0x0000022FA976BDC8>, 'in': <function test_in at 0x0000022FA976BE58>, '==': <built-in function eq>, 'eq': <built-in function eq>, 'equalto': <built-in function eq>, '!=': <built-in function ne>, 'ne': <built-in function ne>, '>': <built-in function gt>, 'gt': <built-in function gt>, 'greaterthan': <built-in function gt>, 'ge': <built-in function ge>, '>=': <built-in function ge>, '<': <built-in function lt>, 'lt': <built-in function lt>, 'lessthan': <built-in function lt>, '<=': <built-in function le>, 'le': <built-in function le>} globals {'range': <class 'range'>, 'dict': <class 'dict'>, 'lipsum': <function generate_lorem_ipsum at 0x0000022FA968CDC8>, 'cycler': <class 'jinja2.utils.Cycler'>, 'joiner': <class 'jinja2.utils.Joiner'>, 'namespace': <class 'jinja2.utils.Namespace'>} loader None cache <LRUCache {}> bytecode_cache None auto_reload True policies {'compiler.ascii_str': True, 'urlize.rel': 'noopener', 'urlize.target': None, 'urlize.extra_schemes': None, 'truncate.leeway': 5, 'json.dumps_function': None, 'json.dumps_kwargs': {'sort_keys': True}, 'ext.i18n.trimmed': False} extensions {} is_async False block_start_string {% block_end_string %} variable_start_string {{ variable_end_string }} comment_start_string {# comment_end_string #} line_statement_prefix None line_comment_prefix None trim_blocks False lstrip_blocks False newline_sequence keep_trailing_newline False undefined <class 'jinja2.runtime.Undefined'> optimized True finalize None autoescape True filters {'abs': <built-in function abs>, 'attr': <function do_attr at 0x0000022FA9756798>, 'batch': <function do_batch at 0x0000022FA9752678>, 'capitalize': <function do_capitalize at 0x0000022FA97401F8>, 'center': <function do_center at 0x0000022FA974C948>, 'count': <built-in function len>, 'd': <function do_default at 0x0000022FA974C828>, 'default': <function do_default at 0x0000022FA974C828>, 'dictsort': <function do_dictsort at 0x0000022FA974C4C8>, 'e': <built-in function escape>, 'escape': <built-in function escape>, 'filesizeformat': <function do_filesizeformat at 0x0000022FA974CEE8>, 'first': <function sync_do_first at 0x0000022FA974CDC8>, 'float': <function do_float at 0x0000022FA97523A8>, 'forceescape': <function do_forceescape at 0x0000022FA9736D38>, 'format': <function do_format at 0x0000022FA9752438>, 'groupby': <function sync_do_groupby at 0x0000022FA9756168>, 'indent': <function do_indent at 0x0000022FA97520D8>, 'int': <function do_int at 0x0000022FA9752318>, 'join': <function sync_do_join at 0x0000022FA974CAF8>, 'last': <function do_last at 0x0000022FA974CC18>, 'length': <built-in function len>, 'list': <function sync_do_list at 0x0000022FA97565E8>, 'lower': <function do_lower at 0x0000022FA9740048>, 'map': <function sync_do_map at 0x0000022FA9756A68>, 'min': <function do_min at 0x0000022FA974C708>, 'max': <function do_max at 0x0000022FA974C798>, 'pprint': <function do_pprint at 0x0000022FA974CF78>, 'random': <function do_random at 0x0000022FA974CE58>, 'reject': <function sync_do_reject at 0x0000022FA9756EE8>, 'rejectattr': <function sync_do_rejectattr at 0x0000022FA975A3A8>, 'replace': <function do_replace at 0x0000022FA973AA68>, 'reverse': <function do_reverse at 0x0000022FA9756708>, 'round': <function do_round at 0x0000022FA97528B8>, 'safe': <function do_mark_safe at 0x0000022FA9756438>, 'select': <function sync_do_select at 0x0000022FA9756CA8>, 'selectattr': <function sync_do_selectattr at 0x0000022FA975A168>, 'slice': <function sync_do_slice at 0x0000022FA9752828>, 'sort': <function do_sort at 0x0000022FA974C558>, 'string': <built-in function soft_str>, 'striptags': <function do_striptags at 0x0000022FA9752558>, 'sum': <function sync_do_sum at 0x0000022FA97563A8>, 'title': <function do_title at 0x0000022FA974C288>, 'trim': <function do_trim at 0x0000022FA97524C8>, 'truncate': <function do_truncate at 0x0000022FA9752168>, 'unique': <function do_unique at 0x0000022FA974C5E8>, 'upper': <function do_upper at 0x0000022FA9740288>, 'urlencode': <function do_urlencode at 0x0000022FA973A1F8>, 'urlize': <function do_urlize at 0x0000022FA9752048>, 'wordcount': <function do_wordcount at 0x0000022FA9752288>, 'wordwrap': <function do_wordwrap at 0x0000022FA97521F8>, 'xmlattr': <function do_xmlattr at 0x0000022FA9740168>, 'tojson': <function do_tojson at 0x0000022FA975A1F8>, 'datetime': <function datetime_filter at 0x0000022FA96344C8>} tests {'odd': <function test_odd at 0x0000022FA966B1F8>, 'even': <function test_even at 0x0000022FA975D0D8>, 'divisibleby': <function test_divisibleby at 0x0000022FA975DD38>, 'defined': <function test_defined at 0x0000022FA97618B8>, 'undefined': <function test_undefined at 0x0000022FA9761AF8>, 'filter': <function test_filter at 0x0000022FA97645E8>, 'test': <function test_test at 0x0000022FA9764558>, 'none': <function test_none at 0x0000022FA966F4C8>, 'boolean': <function test_boolean at 0x0000022FA976B678>, 'false': <function test_false at 0x0000022FA976B708>, 'true': <function test_true at 0x0000022FA976B798>, 'integer': <function test_integer at 0x0000022FA976B828>, 'float': <function test_float at 0x0000022FA976B8B8>, 'lower': <function test_lower at 0x0000022FA976B948>, 'upper': <function test_upper at 0x0000022FA976B9D8>, 'string': <function test_string at 0x0000022FA976BA68>, 'mapping': <function test_mapping at 0x0000022FA976BAF8>, 'number': <function test_number at 0x0000022FA976BB88>, 'sequence': <function test_sequence at 0x0000022FA976BC18>, 'iterable': <function test_iterable at 0x0000022FA976BD38>, 'callable': <built-in function callable>, 'sameas': <function test_sameas at 0x0000022FA976BCA8>, 'escaped': <function test_escaped at 0x0000022FA976BDC8>, 'in': <function test_in at 0x0000022FA976BE58>, '==': <built-in function eq>, 'eq': <built-in function eq>, 'equalto': <built-in function eq>, '!=': <built-in function ne>, 'ne': <built-in function ne>, '>': <built-in function gt>, 'gt': <built-in function gt>, 'greaterthan': <built-in function gt>, 'ge': <built-in function ge>, '>=': <built-in function ge>, '<': <built-in function lt>, 'lt': <built-in function lt>, 'lessthan': <built-in function lt>, '<=': <built-in function le>, 'le': <built-in function le>} globals {'range': <class 'range'>, 'dict': <class 'dict'>, 'lipsum': <function generate_lorem_ipsum at 0x0000022FA968CDC8>, 'cycler': <class 'jinja2.utils.Cycler'>, 'joiner': <class 'jinja2.utils.Joiner'>, 'namespace': <class 'jinja2.utils.Namespace'>} loader <jinja2.loaders.FileSystemLoader object at 0x0000022FA94A7588> cache <LRUCache {}> bytecode_cache None auto_reload True policies {'compiler.ascii_str': True, 'urlize.rel': 'noopener', 'urlize.target': None, 'urlize.extra_schemes': None, 'truncate.leeway': 5, 'json.dumps_function': None, 'json.dumps_kwargs': {'sort_keys': True}, 'ext.i18n.trimmed': False} extensions {} is_async False
内容比较多,我们可以把使用key,value的输出放到两个文件中使用diff命令进行对比
我们把内容方到两个文件中env_default env_change
很明显改变了3处
4,设置路由
add_routes_split.py
import logging logging.basicConfig(level=logging.INFO) import asyncio import inspect from aiohttp import web from urllib import parse import orm from config import configs # 以下是RequestHandler需要定义的一些函数 # 定义函数传递参数为函数,然后取出参数进行判断 # 如果函数参数包含关键字参数并且关键字参数值为空则返回关键字参数的元组 def get_required_kw_args(fn): args = [] params = inspect.signature(fn).parameters for name, param in params.items(): 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) # 判断函数是否有关键字参数,如果有则返回True def has_named_kw_args(fn): params = inspect.signature(fn).parameters for name, param in params.items(): if param.kind == inspect.Parameter.KEYWORD_ONLY: return True # 判断函数是否有字典参数,如果有则返回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 # 判断函数的参数有没有request,如果有request参数则把found赋值为True并结束本次循环继续判断其他参数 # 如果其他参数不是可变参数,也不是关键字参数,也不是字典参数则抛出错误 # 例如响应函数index(request)只有一个参数request所以在执行第一个if以后就没有参数循环了,退出整个循环返回found的值为True def has_request_arg(fn): sig = inspect.signature(fn) params = sig.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): raise ValueError('request parameter must be the last named parameter in function: %s%s' % (fn.__name__, str(sig))) return found ## 定义RequestHandler从URL函数中分析其需要接受的参数 class RequestHandler(object): def __init__(self, app, fn): self._app = app self._func = fn self._has_request_arg = has_request_arg(fn) self._has_var_kw_arg = has_var_kw_arg(fn) self._has_named_kw_args = has_named_kw_args(fn) self._named_kw_args = get_named_kw_args(fn) self._required_kw_args = get_required_kw_args(fn) logging.info('RequestHandler init fn is %s,init after __dict__ is %s' %(fn.__name__,self.__dict__)) async def __call__(self, request): kw = None if self._has_var_kw_arg or self._has_named_kw_args or self._required_kw_args: if request.method == 'POST': if not request.content_type: return web.HTTPBadRequest(text='Missing Content-Type.') ct = request.content_type.lower() if ct.startswith('application/json'): params = await request.json() if not isinstance(params, dict): return web.HTTPBadRequest(text='JSON body must be object.') kw = params elif ct.startswith('application/x-www-form-urlencoded') or ct.startswith('multipart/form-data'): params = await request.post() kw = dict(**params) else: return web.HTTPBadRequest(text='Unsupported Content-Type: %s' % request.content_type) if request.method == 'GET': qs = request.query_string if qs: kw = dict() for k, v in parse.parse_qs(qs, True).items(): kw[k] = v[0] if kw is None: kw = dict(**request.match_info) else: if not self._has_var_kw_arg and self._named_kw_args: # remove all unamed kw: copy = dict() for name in self._named_kw_args: if name in kw: copy[name] = kw[name] kw = copy # check named arg: for k, v in request.match_info.items(): if k in kw: logging.warning('Duplicate arg name in named arg and kw args: %s' % k) kw[k] = v if self._has_request_arg: kw['request'] = request # check required kw: if self._required_kw_args: for name in self._required_kw_args: if not name in kw: return web.HTTPBadRequest(text='Missing argument: %s' % name) logging.info('fn is "RequestHandler.__call__" 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) 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('fn is "add_route" 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_routes函数,自动把handler模块的所有符合条件的URL函数注册了 def add_routes(app, module_name): logging.info('fn is "add_routes"') 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) from app import init_jinja2,datetime_filter app = web.Application() # 把app和filters=dict(datetime=datetime_filter)作为参数传递给初始化函数init_jinja2 init_jinja2(app, filters=dict(datetime=datetime_filter)) add_routes(app, 'handlers') # 打印经过add_routes函数处理的app路由信息,本次添加了路由对应的路径为 / 对应的函数是handler的函数index print(app.router.__dict__) print(app.router.__dict__['_resources'][0].__dict__)
模板文件hanlders.py
为了便于分析我们只保留一个index函数,返回的模板html是test,html
from models import User from coroweb import get import asyncio import logging; logging.basicConfig(level=logging.INFO) @get('/') async def index(request): logging.info('fn is "index" request is: %s' % str(request)) users = await User.findAll() return { '__template__': 'test.html', 'users': users }
设置路由究竟执行了那些操作,下面我们来分析
使用调试模式分析,单步跳过直到执行添加路由的函数,传递参数为app和字符串hanlders
获取模板对象的示例代码
mod = __import__('handlers') # 获得一个模块对象 print(mod) # 打印模块对应的方法和属性 print(dir(mod,locals(),globals())) # ['User', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'asyncio', 'get', 'index', 'logging']
省略若干步骤,直到遍历到函数index
继续循环dir(mod)因为剩下的参数都不满足条件,省略若干步骤
执行完以后往app添加了一个路由,如果模块handlers有多个对应的函数则会添加多个路由
本次add_route最后的参数传递的不是一个函数,而是一个经过实例化的实例,这个实例内部定义了__call__方法,可以通过实例直接调用,关于__call__方法使用可以参考:https://www.cnblogs.com/minseo/p/14965987.html
5,设置静态目录
往app内添加静态目录static用于存放css,js等文件
add_static(app)
其中add_static函数如下
def add_static(app): path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static') app.router.add_static('/static/', path) logging.info('fn is "add_static" add static %s => %s' % ('/static/', path))
执行完毕打印相关信息
import os def add_static(app): path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static') app.router.add_static('/static/', path) logging.info('fn is "add_static" add static %s => %s' % ('/static/', path)) add_static(app) print(app.router.__dict__)
打印输出
{'_frozen': False, '_resources': [<PlainResource />, <StaticResource /static -> WindowsPath('D:/learn-python3/实战/awesome-python3-webapp/www/static')>], '_named_resources': {}}
可以看到往属性'_resources'里面增加一个1个元素 <StaticResource /static -> WindowsPath('D:/learn-python3/实战/awesome-python3-webapp/www/static')>
6,启动app
启动代码如下
runner = web.AppRunner(app) await runner.setup() srv = web.TCPSite(runner, '0.0.0.0', 9000) logging.info('fn is "init" server started at http://127.0.0.1:9000...') await srv.start()
启动程序以后,服务器端等待客户端请求request,服务器端收到请求处理以后把结果返回给客户端浏览器。
下面简单分析客户端请求发送过来服务器端如何处理返回的,想要了解相对详细的request处理过程参考:https://blog.csdn.net/gettogetto/article/details/73028684 通过源码分析reques过程,我暂时没有看懂,回头在研究。
在客户端使用浏览器访问,输出如下
页面源码如下
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Test users - Awesome Python Webapp</title> </head> <body> <h1>All users</h1> <p>Test / test@qq.com</p> </body> </html>
服务器端是什么根据客户端的request请求返回的,下面来逐步分析,本文只是简单分析,深入的请求过程不分析
我们通过在一些位置打印日志输出来分析执行过程
INFO:root:fn is "logger_factory" Request: GET / INFO:root:fn is "response_factory" Response handler... handler is <function AbstractRoute.__init__.<locals>.handler_wrapper at 0x000001771F16C948> INFO:root:request.match_info is <MatchInfo {}: <ResourceRoute [GET] <PlainResource /> -> <function AbstractRoute.__init__.<locals>.handler_wrapper at 0x000001771F16C948>> INFO:root:fn is "RequestHandler.__call__" call with args: {'request': <Request GET / >} INFO:root:fn is "index" request is: <Request GET / > INFO:root:SQL:select `id`, `email`, `passwd`, `admin`, `name`, `image`, `created_at` from `users` INFO:root:rows returned: 1 r is {'__template__': 'test.html', 'users': [{'id': '001637895583499da19dda790484f7e8938266ecc126496000', 'email': 'test@qq.com', 'passwd': '1234567890', 'admin': 0, 'name': 'Test', 'image': 'about:blank', 'created_at': 1637895583.49905}]} r is {'__template__': 'test.html', 'users': [{'id': '001637895583499da19dda790484f7e8938266ecc126496000', 'email': 'test@qq.com', 'passwd': '1234567890', 'admin': 0, 'name': 'Test', 'image': 'about:blank', 'created_at': 1637895583.49905}]} INFO:aiohttp.access:127.0.0.1 [02/Dec/2021:02:20:08 +0000] "GET / HTTP/1.1" 200 361 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36 Edg/96.0.1054.29"
首先是两个数据处理的函数输出
INFO:root:fn is "logger_factory" Request: GET / INFO:root:fn is "response_factory" Response handler... handler is <function AbstractRoute.__init__.<locals>.handler_wrapper at 0x000001771F16C948>
为什么请求过来先调用这两个函数,是在init中定义的,相当于过滤函数,所有请求过来都先经过middlewares参数定义的函数过滤一遍,关于middlewares的工作方式参考:https://blog.csdn.net/sodawaterer/article/details/70170012 我也没看懂
app = web.Application(middlewares=[ logger_factory, response_factory ])
接下来调用的是类RequestHandler的__call__方法,因为在add_route的时候传递的函数参数是RequestHandler的实例,然后再类里面定义了__call__方法,使得实例具有调用属性即可以通过callable判断为True,这个实例具有函数的功能
分析__call__代码
日志输出如下
INFO:root:request.match_info is <MatchInfo {}: <ResourceRoute [GET] <PlainResource /> -> <function AbstractRoute.__init__.<locals>.handler_wrapper at 0x000001771F16C948>> INFO:root:fn is "RequestHandler.__call__" call with args: {'request': <Request GET / >}
日志输出如下
INFO:root:fn is "index" request is: <Request GET / >
日志输出如下
INFO:root:SQL:select `id`, `email`, `passwd`, `admin`, `name`, `image`, `created_at` from `users` INFO:root:rows returned: 1
继续分析执行过程
下面我们来分析返回的二进制码的执行过程
body=app['__templating__'].get_template(template).render(**r).encode('utf-8')
需要使用到上面分析初始化jinjia2模板的代码
修改代码D:\learn-python3\实战\awesome-python3-webapp\www\init_jinja2_split.py
import logging; logging.basicConfig(level=logging.INFO) from datetime import datetime from aiohttp import web import os from jinja2 import Environment, FileSystemLoader def init_jinja2(app, **kw): logging.info('init jinja2...') # 定义一个字典,用于修改jinja2默认的Environment # 这里实际修改了autoescape,默认为False,修改为True options = dict( 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) ) print(options) # 从传递的字典kw中获取path属性 # 本次没有传递,使用为空 path = kw.get('path', None) # path是空的所以执行了if下的语句 # 其中os.path.abspath(__file__)获取当前执行文件的绝对路径 本次为 d:\learn-python3\实战\awesome-python3-webapp\www\init_jinja2_split.py # os.path.dirname(os.path.abspath(__file__))通过绝对路径获取到文件夹 本次为 d:\learn-python3\实战\awesome-python3-webapp\www\ # os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates')把文件夹和'templates'拼接 本次为 d:\learn-python3\实战\awesome-python3-webapp\www\templates # 即相当于执行以下代码获得了当前执行的py文件的文件夹下的文件夹templates完整路径 if path is None: path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates') logging.info('set jinja2 template path: %s' % path) # 修改默认的Environment()默认为一个字典存储了各种键值对,以键值对或者字典的方式传递 # FileSystemLoader为文件系统加载器,不需要模板文件存在某个Python包下,可以直接访问系统中的文件 # 如果默认的Environment()没有传递的键值对则添加,如果有则修改 # 例如默认的有key为'autoescape'对应的value为False options字典有这个key但是值为True所以修改了这个属性 env = Environment(loader=FileSystemLoader(path), **options) #print(env.__dict__) print(kw) # 从传递的字典中去查找属性filters,本次是有传递的不为空对应的值是一个字典{'datetime':datetime_filter} filters = kw.get('filters', None) # 不为空,所以执行if语句,把这个字典的key,value添加至默认env的属性'filter'中 # 默认的属性'filter'值为一个字典,字典的key,value为方法名和对应的一个函数 if filters is not None: for name, f in filters.items(): env.filters[name] = f # 把修改后的env赋值给app的属性'__templating__' app['__templating__'] = env # 时间转换函数,传递一个时间戳计算与当前时间的相差时间,如果相差查过小于60秒则返回1分钟前,以此类推,如果相差查过365天则返回确切的时间戳对应的时间 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) # 通过web生成一个app app = web.Application() # 把app和filters=dict(datetime=datetime_filter)作为参数传递给初始化函数init_jinja2 init_jinja2(app, filters=dict(datetime=datetime_filter)) # 为了对比dict1存储了默认的Enviroment().__dict__ # dict1存储了进过初始化修改以后的__dict__ dict1 = Environment().__dict__ dict2 = app['__templating__'].__dict__ # 为了便于对比使用key,value的方法打印 # for k,v in dict1.items(): # print(k,v) # 只打印cache信息进行对比 for k,v in dict2.items(): if k == 'cache': print(k,v) # 自定义字典 r = {'__template__': 'test.html', 'users': [{'id': '001637895583499da19dda790484f7e8938266ecc126496000', 'email': 'test@qq.com', 'passwd': '1234567890', 'admin': 0, 'name': 'Test', 'image': 'about:blank', 'created_at': 1637895583.49905}]} template = r.get('__template__') #print(template) print("#### app['__templating__']") # 打印jinjia2的env,这里是经过修改的然后发到app的属性'__templating__'中,没有修改默认的是Environment() print(app['__templating__']) # <jinja2.environment.Environment object at 0x0000021289BD1408> print("#### app['__templating__'].get_template(template))") # get_template获取模板对象,获取的前提是这个变量已经把需要查找模板的路径定义好了,这个代码实现了查找模板的路径 env = Environment(loader=FileSystemLoader(path), **options) # 其中本次的path为 d:\learn-python3\实战\awesome-python3-webapp\www\templates 如果没有定义模板查找的路径则会因为找不到模板报错 # 返回为模板对象,然后再字典属性'cache'中添加对应的信息 print(app['__templating__'].get_template(template)) # <Template 'test.html'> dict3 = app['__templating__'].__dict__ # render方法,往模板添加对象,当模板使用时调用它 # 相当于往模板test.html中添加r这个字典,然后再模板中jinja语法去,调用这个字典 # 例如模板test.html中有以下代码 {% for u in users %}则就是遍历users这个变量然后从中取出想要的值例如u.name,u.email print("app['__templating__'].get_template(template).render(**r)") print(app['__templating__'].get_template(template).render(**r)) # 二进制编码,页面返回需要编码后返回 # print(app['__templating__'].get_template(template).render(**r).encode('utf-8')) # 打印获得get_template('test.html')后属性'cache'去和没有使用get_template对比 for k,v in dict3.items(): if k == 'cache': print(k,v)
输出如下
INFO:root:init jinja2... {'autoescape': True, 'block_start_string': '{%', 'block_end_string': '%}', 'variable_start_string': '{{', 'variable_end_string': '}}', 'auto_reload': True} INFO:root:set jinja2 template path: d:\learn-python3\实战\awesome-python3-webapp\www\templates {'filters': {'datetime': <function datetime_filter at 0x000001D622C22E58>}} cache <LRUCache {}> #### app['__templating__'] <jinja2.environment.Environment object at 0x000001D622532D48> #### app['__templating__'].get_template(template)) <Template 'test.html'> app['__templating__'].get_template(template).render(**r) <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Test users - Awesome Python Webapp</title> </head> <body> <h1>All users</h1> <p>Test / test@qq.com</p> </body> </html> cache <LRUCache {(<weakref at 0x000001D622DC8728; to 'FileSystemLoader' at 0x000001D622C420C8>, 'test.html'): <Template 'test.html'>}>
对比分析如下
我们可以看到模板test.html接收了字典参数,把对应的jinjia语法的字符不显示或者进行了替换然后返回了
对比模板是返回给html页面的源码
本人水平有限,有些地方在表述方面可能不专业或者描述不当,望谅解!