本片博客仅用于记录开发个人博客中的心得体会。
目标:
使用aiohttp, aiomysql开发异步服务器。aiohttp中文文档
第一阶段:开发orm框架
orm框架的作用就是直接在代码中对对象进行操作,映射到数据库的操作,这样就屏蔽了数据库操作的具体细节。相当于hibernate的作用。
第一步:
使用了aiohttp那么所有的I/O都要用异步技术,数据库也不例外,要用aiomysql。
首先使用aiomysql模块,创建一个数据库连接池和crud方法。 这个根据aiomysql模块很容易实现。
第二步:
orm框架的基本原理就是用metaclass在实例化对象之前去解析出映射关系。可以参考之前写的简单实现
为了让我们的orm框架也是异步的,那么就需要在Model类的crud方法中,调用前一步的数据库连接池和crud方法。
所以思路就是:orm框架解析映射,拼凑sql,然后调用aiomysql数据库连接池真正去执行。
封装顺序依次是,aiomysql利用asyncio封装了mysql-connector, 我们首先封装aiomysql提供一个连接和数据库操作,然后再用orm框架去封装aiomysql。提供直接操作对象的异步数据库操作。
需要注意的是orm的工作基本原理:假设User(Model)
- User继承了基类Model,所以其__new__交给了ModelMetaclass去创建,于是ModelMetaclass就把User指定的静态属性(也就是映射)保存到__mappings__(这里的属性是齐全的,User类定义了多少,就是多少),之后把静态属性都删掉。
- User类被实例化之后,就通过__mappings__保存了映射,(当然还有__table__,__fields__,都是一样的原理)。当想要进行数据库操作时,需要两个内容,列和值。列已经保存在__mappings__中,值是通过User类初始化参数提供的。
- 因为User是dict, 所以初始化参数有可能不齐全,所以需要在映射关系中提供默认值,这个默认值可以是函数,去自动计算日期或者主键。当dict获取不到需要的列时,证明User实例化的时候没有提供,这时就去__mappings__(这里肯定存在,上面说了)中找,使用默认值。
- 当调用查询操作的时候,返回值是dict,把dict作为关键字参数传递给User类 就创建了一个User类。
第三步:
开发需要的Model类,这个就很简单了。根据数据库中的表去开发即可。相当于Java中的bean。
第二阶段:开发web框架
说开发有些言过其实,因为aiohttp比较底层,用起来会比较麻烦,所以封装起来,方便使用。不能理解的话,把一个基本的aiohttp开发服务器的用法写出来,然后看着封装会很有帮助,最简单用法aiohttp最简单服务器。
第一步:
为url处理函数提供@get(path)和@post(path)带参数的装饰器 方法见之前一篇博客python装饰器。作用是为处理函数绑定url和请求方法。
就像 __method__='GET' __route__ = path这样。
第二步:
aiohttp提供的路由是app.router.add_route('GET', '/', index) 这样的,需要一个个手动设置。上一步每个处理函数都带上了url和method,也就是说这三个参数都包含在处理函数中了。
- 封装一个add_route(fn)方法 去添加路由
- 把处理函数都放到一个文件里,再封装一个add_routes('handlers') 去扫描这个文件,对符合要求的函数 调用add_route(fn)注册路由
第三步:
这时,只用在handlers中写处理函数即可,但是处理函数 fn(request):的参数只有一个request,我们必须去自己从request中获取想要的参数。所以可以考虑像java中spring mvc,在处理函数的参数列表中添加想要的参数,就可以在函数体中直接使用这个参数。也就是说,需要去写一个类RequestHandler获取到fn想要的参数,然后再调用fn去处理逻辑。
- __init__ RequestHandler(fn) 获取fn参数列表的各种信息
- __call__ 封装fn,获取fn的参数信息,然后调用fn
- add_route(fn) 内部实际上添加的路由处理函数应该是 RequetsHandler(fn)
第四步:
获取完request中的数据,进行了业务处理之后,就是返回。aiohttp返回web.Response(...) 。当然要用上模板,这里选jinja2。我们需要继续封装成这样去返回响应。
return {
'__template__': 'test.html',
'data': data
}
提供模板和数据。
那么就需要在返回响应之后,有一个拦截器去拦截住这个dict去进行解析,把它做成web.Response 并套用上指定的模板和数据。
aiohttp里的拦截器叫做中间件。编写好中间件,创建web服务器的时候直接传进去即可。
所以,我们编写一个response_factory中间件去做上述的过程。
第三阶段:前端框架
前端这个问题说起来其实没有太大难度,但是很费事儿,毕竟前端是脸面,想要一个好看的UI真得花大把时间。之前我只用过bootstrap, 这次项目中使用的UIkit2。项目完成后计划更新到UIkit3。
第一步:
还是通用的页眉和页脚, 如果是纯html或jsp页面, 那就用标签直接引入就好了。但是我们用了jinja2模板有一个遍历,可以像类那样去继承,去覆盖父类。所以第一步,先写一个__base__.html 里面写好通用的页眉和页脚,还有需要覆盖的地方用{% block xxx %} {% endblock %}指明
第二步:
开始写需要的模板xxx.html 使用{% extends '__base__.html'%} 继承模板。 然后在{% block xxx %} {% endblock %} 里面实现覆盖。
第四阶段:实现功能
当真正写功能的时候,就会涉及到大量前端的东西,我们这里需要用到vue.js和jquery。去绑定数据和进行交互。做到这里觉得很难,后来深入做进去之后,发现难是因为vue和jquery不熟悉,所以就搞不懂前后台怎么交互的。只能去熟悉vue和jquery。
第一步:用户注册登陆
注册登陆退出,想简单的实现这个功能也是很简单的,并且一般学习也就止步于此了。
先看注册:
注册的逻辑很简单:
- 注册页面提交注册信息到服务器
- 服务器检查注册信息,合法就构建成User对象,调用orm框架save进数据库
需要注意:
- 用户提交的password 在html页面提交的时候 已经加密, sha1(email + ':' + password)
- 数据库保存的正是加密后的password
再看登陆:
登陆也就是要求保持http有状态,能识别我是登陆过的或者没登陆过的,http本身无状态需要借助session或cookie来保存状态。可参考http的无状态
思路是这样的,每个url请求都去验证cookie。具体做法如下,
第一次登陆的时候,我肯定是没有登陆过的,这个时候要做的事情有两件:
- 验证账号密码
- 设置cookie ['xxx'],user信息
当我登陆了之后,我再去访问别的url,如果我没有使用cookie,那浏览器就不知道我是否登陆,使用了cookie就可以去验证cookie:
- 获取cookie['xxx'],如果没有,就是没登陆
- 如果有,就可以从中取出user信息
基本逻辑就是上面这个样子,
但是为了做得更安全,还需要在有cookie['xxx']后去确认这个cookie['xxx']是否有效(有可能是伪造的,有可能过期了)
验证cookie['xxx'] 设置的时候在里面加上sha1(uid password max_age 服务器特定的salt)
然后在获取的时候 根据传过来的uid 查询去用户信息去计算sha1(..) 和cookie['xxx']中传过来的sha1(...)对比即可验证。
为了做得更简单,每个url都需要搞上面的的一套验证才能正确取出useri信息,所以用拦截器统一处理。
做一个中间件,auth_factory去做上面这一系列验证,并把验证结果绑定到request.__user__去。
这样每个url处理函数只需要看request.__user__是否为null 就知道用户是否登陆,并且直接获取用户信息了。
如果登陆了,就把request.__user__绑定到模板就行了。关于绑定到模板这个事情,也不用自己在每个handler中写,直接在response_factory 添加 request.__user__ 这个属性就好了。
退出
退出相比登陆就简单多了,只需要把cookie['xxx'] 的max_age设置为0即可。
第二步:博客创建,查阅
vue jquery jinja2 uikit 混合起来 如果不熟悉其中任何一个,很容易就不知道数据是如何传递的了。
首先分析一下handler.py里都有些什么:
大致可以分为三类:
- 操作资源的,作为web service的,rest风格的 api。这都是一个意思,就是crud数据库中的数据,返回的json格式。提供这类api,可以方便web上的其他人,其他应用调用。
- 普通的get url处理函数 主要用来访问html
- 普通的函数,一些通用的功能,提取出来,抽象成函数。
来看查阅博客的流程:
- 访问url:get /manage/blogs ==> manage_blogs.html
- html上的JQuery加载运行:manage_blogs.html ==> 访问url: /api/blogs 获取数据库中所有博客
- 获取数据后,vue绑定view和model
从上述流程可以看到,访问html和访问数据不仅在handler上分开了,在html页面页分开了,这样就可以先看到html静态页面,然后等数据请求完毕后,再看到数据加载。更快。
接着看创建博客流程:
- 操作html: mange_blogs.html 访问 url: get /manage/blogs/create ==> manage_blog_edit.html
- 操作html 提交表单 访问url: post: /api/blogs 保存提交的博客
连续看这两个流程,就很清楚了:
就是先去访问url到达html页面,然后访问rest api操作数据。
需要注意的是:访问url(rest api也是url)有很多种方法:form表单,Jquery请求, vue的方法,直接在地址栏输入数据。所以真正该记住的是先访问页面,再加载数据的思想。这样才能以知道为什么要设计rest api,以及如何使用它们。
第三步:
开发完了博客的crud,剩下就是苦力活了。照葫芦画瓢就好。分别要完成:
评论的管理: curd
用户的管理:在前面的基础上加上查询就可以了。
完工。撒花。
总结:
功能列表:
普通用户:
- 注册
- 登陆
- 查阅博客
- 评论博客
管理员用户:
- 创建博客
- 删除博客
- 更改博客
- 查阅评论
- 删除评论
- 查看用户