项目地址博客地址第一个版本的Frodo已经实现,在下一个版本之前,我会把目前的开发思路整理成三篇文章,他们分别是数据文章、通信文章和异步文章。这篇文章谈到实现具体功能的逻辑过程。在Web应用的总结中,我个人更喜欢将业务流程称为“通信”。因为整个过程都是从后台到前端整理和处理数据,所以这个过程的协议可以不同(http(s)、websocket),方法可以不同(rcp、ajax、mq),返回内容的格式可以不同(json、xml、html(模板)、早年的Flash等);刚才讲的是前后端通信。实际上涉及到逻辑模块、进程甚至后续容器之间的通信。本文首先介绍Web通信的核心,前后端通信。模板技术与前后端分离模板技术:本世纪头十年广泛应用的Web技术,他有一个比较著名的名字MVC模式。核心思想是使用后端代码在html模板中写入数据,模板引擎返回渲染后的html。Django嵌入了这项技术,其他python框架需要依赖jinjia、Mako等单独的模板。java等其他语言的JSP也是采用这种模式。它的特点是操作直接,在需要的地方直接写入相应的数据。也可以直接使用后端语言在页面上编写逻辑,开发速度快。但缺点也很明显。前后端耦合严重,维护困难,不适合大型项目。协议:http方法:可接受内容:html(templates)前后端分离:目前主流的模式,当项目越来越大的时候,前端工程化的需求催生了webpack这个工具。那么Vue、React、Angular框架都是以MVVC模式为主,即只从后端获取数据,将渲染和业务逻辑放到前端框架中。这样,前后端开发人员就可以做到最大程度的分离。Protocol:AllMethod:AllContent:json/xmlMako模板和他的朋友FastAPI-MakoFrodo使用模板制作博客的前台,考虑到这部分页面较少,逻辑简单,后端人员易于维护。模板是完全够用的。_没有过时的技术,只有不合适的技术_。Mako是主流的python模板之一。它的原生接口可以直接使用,但是需要封装一些重复的逻辑:模板中固定的几个上下文变量请求对象(后端框架使用的请求对象,在Flask中,Django和fastapi都存在),模板需要用到他的一些方法和属性,比如反向寻址request.url_for(),request.host,甚至request.Session中的内容请求上下文context(主要是指body,接触过的朋友)web开发可以列出主要请求体:Formdata、QueryParam、PathParam,这些可能在模板中用到)返回上下文(叶涛不提供)模板文件自动寻址静态文件寻址模板异常处理同Flask,以及fastapi的路由也可以正常工作。为了将上面的模板函数封装到路由函数中,直接的方式就是使用python装饰器。最后实现如下效果:fromfastapiimportAPIRouter,Request,HTTPExceptionfromfastapi.responsesimportHTMLResponsefrommodelsimportcache,Postfromextimportmako@router.get('/archives',name='archives',response_class=HTMLResponse)@mako.template('archives.html')#指定模板文件名@cache(MC_KEY_ARCHIVES)asyncdefarchives(request:Request):#需要显式传递Requestpost_data=awaitPost.async_filter(status=Post.STATUS_ONLINE)post_obj=[Post(**p)forpinpost_data]post_obj=sorted(post_obj,key=lambdap:p.created_at,reverse=True)rv=dict()foryear,itemsingroupby(post_obj,lambdax:x.created_at.year):ifyearinrv:rv[year].extend(list(items))else:rv[year]=list(items)archives=sorted(rv.items(),key=lambdax:x[0],reverse=True)#只返回上下文return{'archives':archives}其实很容易理解,唯一需要说明的是为什么要显式传递请求,fastapi避免了passing最大程度的请求,这和Flask的思路是一样的,使用Local线程栈可以完全区分不同请求的上下文。但是模板通常需要反向寻址,类似于:%foryear,postsinarchives:${year}%endfor@makoLouisYZK/FastAPI-Mako中完成了简单的装饰器,有兴趣的朋友可以看看。另外还有@cached装饰器,它缓存了函数的返回结果模板。如果当前页面的数据没有变化,下次访问直接从redis中取数据。详细逻辑会在后面的增删改查逻辑中介绍。本节描述了所有数据模型的CRUD通信逻辑。Posts、Activity等个体的数据存储方式多种多样,需要的技巧也比较多。而所有的数据操作都遵循以下流程:控件用例中的DataModel就是数据章节中设计的数据类,他们有几种方法来处理CRUD需求,其中最重要的有两点:生成操作的SQL生成KV数据库缓存上面两点使用的key使用了sqlalchemy和python装饰器的一些tricks。可以参考源码models/base.py和models/mc.py。值得一提的是,更新和删除缓存的实现:##clear_mcmethodinmc.pyasyncdefclear_mc(*keys):redis=awaitget_redis()print(f'Clearcached:{keys}')assertredisisnotNoneawaitasyncio.gather(*[redis.delete(k)forkinkeys],return_exceptions=True)##__flush_inthebaseclass_methodfrommodels.mcimportclear_mc@classmethodasyncdef__flush__(cls,target):awaitasyncio.gather(clear_mc(MC_KEY_ITEM_BY_ID%(target.__class__.__name__,target.id)),target.clear_mc(),return_exceptions=True)##target是一个特定的数据实例。他们重写了clear_mc()方法来删??除和指定不同的键。例如下面Post类的改写:asyncdefclear_mc(self):keys=[MC_KEY_FEED,MC_KEY_SITEMAP,MC_KEY_SEARCH,MC_KEY_ARCHIVES,MC_KEY_TAGS,MC_KEY_RELATED%(self.id,4),MC_KEY_POST_BY_SLUG%self.slug,MC_KEY_ARCHIVE%self.created_at.year]foriin[True,False]:keys.append(MC_KEY_ALL_POSTS%i)fortaginawaittags:keys.append(MC_KEY_TAG%tag.id)awaitclear_mc(*keys)这样可以保证每次创建、更新、删除数据时,都可以删除相关缓存,保持数据一致性。你可能已经注意到,删除缓存的操作是可等待的,也就是说这里可以利用异步来实现并发。于是我们看到了asyncio.gather(*coros)的使用,可以并发删除多个key,因为redis创建了一个连接池,这样就不用多线程了,asyncio就是这样实现io并发的。(其实这一点应该在异步的文章中介绍过,但是这一点很重要)。身份验证身份验证的需求来自两方面:内容管理系统后台只能由博客主操作,如博客发布、密码修改等;访客评论需要验证身份。管理员认证--使用JWTJWT是目前应用广泛的认证方式之一,相对于cookies的优势可以参考相关文章。并且fastapi内置了对JWT的支持,我们用它来做验证非常方便。在讲具体实现之前,还是得了解一下它的通信逻辑:上面的流程代表了登录的逻辑和访问认证API的一般逻辑。你找到问题了吗?令牌存储在哪里?令牌存在于何处?服务端生成Token客户端接收,下次请求带上。这种使用频率高、数据量小的数据,直接存放在内存中是最合适的。编程语言中共享全局变量是必不可少的,比如multiprocess.Value就解决了这个问题。但是异步是针对eventloop研究的,没有线程进程的概念。这时候contextvar专门用来解决异步变量共享的问题。需要python3.7以上的fastapi来帮助我们维护这个Token。它只需要一个简单的定义如下:fromfastapi。securityimportOAuth2PasswordBeareroauth2_scheme=OAuth2PasswordBearer(tokenUrl='/auth')表示Token生成路径为/auth,使用oauth2_scheme形式作为全局依赖的token来源。每当接口需要使用Token时,只需要:@router.get('/users')asyncdeflist_users(token:str=Depends(oauth2_scheme))->schemas.CommonResponse:users:list=awaitUser.async_all()users=[schemas.User(**u)foruinusers]return{'items':users,'total':len(users)}Depends是fastapi的一个特性。直接写在接口函数的参数中,可以在请求之前执行一些逻辑,类似于中间件。这里的逻辑是检查请求头是否包含Auth:Bear+Token,如果没有则不能发出请求。token生成逻辑在登录界面完成,这几乎是Frodo中最复杂的逻辑:@app.post('/auth')asyncdeflogin(req:Request,username:str=Form(...),password:str=Form(...)):user_auth:schemas.User=\awaituser.authenticate_user(username,password)如果不是user_auth:raiseHTTPException(status_code=400,detail='IncorrectUserAuth.')access_token_expires=timedelta(分钟=int(config.ACCESS_TOKEN_EXPIRE_MINUTES))access_token=awaituser.create_access_token(data={'sub':user_auth.name},expires_delta=access_token_expires)return{'access_token':access_token,'refresh_token_token':access'bearer'}基本上按照本节中的时序图。访问鉴权--使用Session的访问鉴权有很多,受限于博客内容,访问Frodo的应该都有Github,所以使用他的鉴权,逻辑如下:整个逻辑很简单,按照鉴权逻辑Github的,另一种方式比如微信扫码需要改一下。只需注意跳转的网址即可。同时,JWT不用于存储访问者信息,因为没有过期等限制,sessioncookies是最直接的。@router.get('/oauth')asyncdefoauth(request:Request):如果str(request.url)中出现“错误”:引发HTTPException(status_code=400)client=GithubClient()rv=awaitclient.get_access_token(code=request.query_params.get('code'))token=rv.get('access_token','')try:user_info=awaitclient.user_info(token)除了:returnRedirectResponse(config.OAUTH_REDIRECT_PATH)rv=awaitcreate_github_user(user_info)##使用session存储request.session['github_user']=rvreturnRedirectResponse(request.session.get('post_url'))注意fastapi打开session需要添加中间件fromstarlette.middleware.sessionsimportSessionMiddlewareapp.add_middleware(SessionMiddleware,secret_key='YOURKEY')starlette是什么?不是fastapi吗?简单的说,starlette和fastapi的关系就像werkzurg和Flask的关系一样,WSGI和ASGI的区别,现在ASGI的想法就是要超越WSGI,当然我们还需要开发一套基本标准和工具库。好了,通信逻辑基本就这些了,Frodo使用的通信模型还是很少的。下一篇《异步篇》,既与通信有关,也与数据有关。这就是异步博客和一般python实现的博客的区别。