基于SPA架构的GraphQL工程实践



内容来源:2018 年 6 月 9 日,国内某大型电商公司用户体验部门前端开发专家邓若奇在“杭州第一届 GraphQLParty—GraphQL与领域驱动带来的协同价值”进行《基于SPA架构的GraphQL工程实践》演讲分享。IT 大咖说(微信id:itdakashuo)做为独家视频合做方,经主办方和讲者审阅受权发布。前端

阅读字数:3838 | 10分钟阅读node

获取嘉宾演讲视频及PPT: suo.im/5ebSDK

摘要

主要演讲主要介绍基于SPA架构的GraphQL工程实践,从前端视角来分析GraphQL在整个链路中的协同效率问题。web

GraphQL的哲学

GraphQL是经过一套Schema来定义领域模型,官方称之为SDL。它引入了一套类型系统来对模型进行约束,如上图展现的3个类型。数据库

在实际应用中客户端将要获取的字段经过Schema文本的方式发送给服务端,服务端接收处理后返回json格式的数据。json

GraphQL提供了一套统一模型定义,拥有灵活的按需查询的能力。还有个容易被你们忽视的特性——经过类型系统提供了模型之间的关系描述,由此能够看出虽然数据以json格式返回,可是实际的应用数据呈现的应该是网状架构,这使得GraphQL成为描述应用数据的极佳选择,也是它名字的由来。后端

架构设计与技术选型

从前端视角看先后端分离

以我我的经从来看,先后端分离能够分为4个阶段。浏览器

第一阶段前端异步请求数据接口刷新局部UI。缓存

第二阶段前端接管View层,这是不少基于MVC的框架采用的模式。服务器

第3、四阶段随着nodeJS技术的兴起,先后端的协同效率问题开始受到关注,后续经过引入BFF这层让前端可以快速迭代,同时后端下沉为服务或微服务。微信

上图是个人技术选型方案。前端为React和relay,relay是基于GraphQL和React的数据整合方案。BFF这层引入的是Egg.js,它是阿里开源的面向企业级开发的web框架。

如何设计BFF

基于REST的分层设计

先来看下传统的基于MVC模式的web server受理REST请求过程。首先请求进入middleware(中间件),在此处理一些通用逻辑,好比用户登陆态判断或API鉴权。接着进入Router将请求分布到不一样的controller,controller这层调用model进行业务处理,而后model再调用service层取数据,最后数据在controller层完成封装并返回。

基于GraphQL的分层设计

引入GraphQL以后Router和controller再也不被须要,由于首先GraphQL并不基于endpoint,其次它自身的resolver能够完成数据封装。此架构中咱们引入了两个模块connector和Schema Loader。connector模块一方面针对GraphQL的一些特色作了特殊缓存设计,另外一方面制定了先后端协做的规范。

构建schema

这是我最初写的GraphQL代码,借鉴与GraphQL-js的官方repo。如今看来这段代码存在2个问题,首先schema应与语言无关而只是模型的描述,其次开发的时候应该遵照设计先行的原则,先肯定模型而后再写代码。


理想状况应该是这样的,先肯定模型描述和关系,而后再编写resolver决定具体处理方案,最后在应用加载的时候使用schema Loader将他们绑定在一块儿。

鉴权与受权

鉴权和受权的区别在于,鉴权主要针对通用逻辑,是粗粒度的,受权则是定制逻辑,粒度较细。


在GraphQL中受权可能针对的是某个字段,如图所示query查询的是小明的工资,因为工资只能本身查看,因此要在resolver中加入一段受权逻辑保证查询者为本人。这里的设计理念是将受权逻辑封装在model层,让它在不一样的resolver中得以复用。

缓存设计

上图是数据库中的两条用户记录,他们互为friend,经过两段代码分别查询用户和他们的friend。

这是上面代码请求的时序图,能够看到一共发出了4次请求,但最终获取到的数据只有两条。

引入缓存以后,第二轮的请求就均可以在第一轮的查询缓存中找到。

还能够再进行优化,将两段代码的第一轮请求合并在一块儿,这才是最优解。

为实现以上的效果,首先须要使用缓存。而后还要有请求队列,将同一个周期中的全部load或query所有缓存起来,而后在下一个周期中合并成一个请求放出。最后是批量处理的能力,用于处理附带批量key的请求。

Facabook提供了一种批量处理的解决方案DataLoader,它接收一个用来处理批量key的方法,每一个DataLoader的实例下方都有一个cache。最初的需求在引入DataLoader以后代码以下图所示。

这段代码的最终效果是把三个请求合并成一个请求,在后端执行的是一条SQL语句。

不过在实际结合关系型数据库使用的时候仍是略微有些复杂。通常咱们对关系型数据库进行查询的时候即会依据PK(primary key)也会依据UK(unique key)。如上代码关于用户的查询既能够经过ID也能够经过Mobiles,这就不得不实例化两个DataLoader实例。因为是不一样DataLoader实例,因此用的是不一样的缓存,致使缓存利用率不高。

为此我编写了rdb-dataloader模块,让PK和UK的查询都在同一个实例中,达到复用缓存的目的。注意红框中的代码,这里先经过name查询出一条记录,而后对这条记录经由ID作第二次查询,显然第二次查询不会发出,而是会使用缓存。方案的核心在于缓存记录的所有字段,数据量的控制应该由分页逻辑来关心。

DataLoader是请求级别的缓存,请求进来的时候初始化DataLoader实例,请求结束后就销毁。

先后端如何协做

Relay

做为一名前端在使用GraphQL的时候首先要是思考的是对浏览器的性能有何影响,这也是接下来进一步挖掘relay的缘由。

在使用React组件时,最广泛的诉求就是须要异步取数据,而后对数据进行渲染,常规的作法是在componentDidMount中添加异步取数的逻辑。所以实际应用中随着页面层级的深刻,加载时间会随之变长,子组件必须等待父组件的数据加载完以后才能开始渲染。

对此最简单的优化方案是将全部组件须要的数据所有放在第一次请求中,如上所示。但是在后续要新增需求的时候我却搞出了bug,由于此时已经分不清哪些字段对应哪些组件。

再来看下relay的实现方式,relay有一个creatFragmenContainer方法,能够向该方法传入React组件,而后经过GraphQL的scheam返回relay component。这种方式不只实现了依赖注入也没有打破组件的数据封装性。

在最初的query中嵌入上面的fragment后,咱们就知道了字段是由哪一个组件发出的。

上图是一段伪代码,表示的是relay底层的协做方式。第一个对象是博客,有内容,也有做者,可是这个做者是一个 user 类型,博客不会直接存储 user 的所有数据,而是经过引用的方式引用到第二个对象。同理评论的做者和它属于哪一个博客,一样是用引用的方式。这样的好处在于只要对象发生改动,全部引用该对象的地方都会同步更新。

请注意图中一、二、3这几个数字,他们是全局惟一的缓存key。因为全部的数据都在缓存中,因此不能再使用数据库中的ID,不然对于ID相同的博客和用户就没法处理了。惟一ID的实现有各类方案,可使用base64(type+”:”+id)这种形式。

全局ID须要后端来配合,定义fromGlobalId和toGobalId这两个方法。fromGlobalId负责将relay发请求时带来的ID解包成数据库ID,toGobalId负责返回的时候对数据库ID装包。

客户端将schema文本发送到服务端,而后由服务端进行处理的这一过程当中,文本量实际上是至关大的,对于网络环境很差的用户体验会很是差。


那么能不能直接发送query id,在服务端经过id解析出文本呢?所幸relay提供了这种方式,在构建relay脚本的时候会给模块注入hash标识当前schema,经过这个hash先后端就对应起来了。

须要解决的问题

首要解决的是DOS Attack,说白了就是上图这种嵌套攻击,请注意这并非死循环,这只是一个攻击者故意经过你的 query 无限写的很是复杂的嵌套,让你的服务器消耗殆尽。显然设置query文本长度和query白名单无益于解决问题,正确的作法是控制query的深度。

对于rate limiting限流,因为GraphQL并不是是基于Rest,因此不能经过限制路由每分钟的调用次数来解决。而应该是限制读写操做,上面的例子表示的就是每分钟最多只能添加20个评论,经过directive实现。

不过实际上限流的实现成本是比较大的,若是要专门实现限流功能,须要依赖第三方的一些服务。

相关文章
相关标签/搜索