面对极度复杂的前后端业务场景,使用 GraphQL 正确的姿势
Posted IT大咖说
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面对极度复杂的前后端业务场景,使用 GraphQL 正确的姿势相关的知识,希望对你有一定的参考价值。
内容来源:2018 年 06 月 09 日,有数派联合创始人周文宇在“杭州第一届 GraphQLParty—GraphQL与领域驱动带来的协同价值”进行《Stratup使用GraphQL的姿势》演讲分享。IT 大咖说(微信id:itdakashuo)作为独家视频合作方,经主办方和讲者审阅授权发布。
阅读字数:6109 | 16分钟阅读
摘要
本次演讲主要介绍如何使用GraphQL,分别从前后端两个角度分析GraphQL的优劣势,对比Restful又能够给前后端协同开发带来哪些好处。
获取嘉宾演讲视频及PPT,扫一扫下方二维码即可。
为什么使用GraphQL
之所以要使用GraphQL主要出于几方面的考虑。首先我们的业务复杂度高,应用本身的业务场景极其复杂,涉及到纺织行业大大小小几十个业务场景和十几个不同工种功能之间的联动与交互。
其次是由于Tech Team有一半同事Remote在全国各地,因此对协同效率与开发工具链的要求很高,而GraphQL恰好能解决沟通成本的问题。第三是因为终端过于繁杂,既有b2b的平台也有SaaS产品和硬件产品,各个终端的连接非常多,如果用传统的Restful API开发会用到非常多的胶水代码。
这里大概介绍一下传统接口在我们这个领域中应用所会遇到的一些问题。
沟通成本高。前后端需要反复沟通接口结构和数据类型,但是对于B端这种相对复杂的业务场景,数据和业务场景都是多变的。同时前端对数据接口结构的掌控有限,前后端Pipeline也不一致,传统BBF对前端不够友好,更多的还得依赖后端。
开发效率低。文档维护成本高,一致性和同步性问题很难解决。
一些技术债。比如对多终端和多场景支持不友好,缺乏标准化的约束,前后端都需要重复工作。
以上是从后端的角度来对这些问题进行的分析,接下来由我们的前端方面的负责人介绍下我们在前端方向上的一些实践。
刚才周老师已经从后端的角度分析了这些痛点,我这里就从前端的层面来重新谈论下这些问题。
第一个要提的是反复沟通接口的问题,对于后端来说他们更关注的是数据的表结构,而前端所需要的仅仅是界面的展示数据。有时这些数据并不能够从某个表中直接获取到,可能要跨很多的表,这就需要前端和后端之间进行相互的磨合沟通才能获得最终的结果。
其次是前端对接口的结构掌控有限,当前端的请求发送出去后,接口所返回的数据形式有可能并不符合预期,比如本该返回的数组变成了对象。
再来看下开发效率低的问题。前端开发中UnderFetching和OverFetching一直无法避免。
UnderFetching指的从接口中取到的数据远低于需要展现的数据,由此引发了N+1问题,即需要根据已取得的接口中的ID或者详细信息再去请求对应的接口,这就导致了前端的请求从异步变成了同步。
原本页面的渲染时间是取决于最慢的接口响应时间,而同步模式下则是所有接口串行返回的时间。OverFetching则正好相反,接口的返回数据远高于需要展现的数据,对前端和服务器端的资源造成了双重消耗。
面临传统接口所带来的这些痛点,一般团队都会选择NodeJs作为中间层的解决方案。那么Node和GraphQL相比有那些优缺点呢?下面来一起看下。
Node.js在一定程度上减轻了underFetching和overFetching问题,尤其是解决了N+1的问题,后端可以通过Node.Js中间层来进行数据资源的整合,但是此时仍旧只能返回一个固定的数据结构。
Node.js缺陷也很明显,比如对多端需要做额外的适配,难以适应前端的快速迭代,需要花费大量时间维护,由此还会引发接口文档的断层。
GraphQL对于Node碰到的这些问题基本上都能够很好的解决。GraphQL中可以由前端来定义Query,页面和数据能完美匹配。同时一旦Schema确定,前后端就可以快速并行开发。前端对字段及返回类型也能够了如指掌,GUI清晰的展现了字段的类型结构。
给高速运行的汽车更换轮子是一件相当危险的事情,而我们早期所面临的情况和这差不多。当时我们的产品已经推出有一段时间了,数据库非常庞大,功能也很繁杂,而且数据完全是基于Restful。此时需要将Restful完全替换成GraphQL,这无疑对前后端来说都是一个非常大的挑战。
在跟换轮子之前,我先比较了一下前端数据缓存的框架,目前主流的有Relay和Apolloo。Relay由Facebook官方推出,支持的框架有React和React Native,Apolloo则支持绝大多数主流框架。
Router方面Relay官方支持React Route,新版本中还支持一个新的路由Found。Apollo由于本身的运行方式和生命周期已经完全和路由割离开,所以能够支持任何Route。
Relay的数据缓存由官方提供的RelayStore完成,Apollo则是基于Redux。基于以上几点考虑,我最终选择了Apollo。
之前我们在使用Redux加Redux-saga的时候,倒没有遇到什么特别大的问题,主要还是特别麻烦。需要手动管理和存储返回数据,还要为每个资源建立一套Action,Reducer,Redux-saga,同时要针对每个请求进行异常及数据处理,针对页面需要的数据触发多次串行请求。
在使用GraphQL和Apollo之后,前端方面只需要全局定义一个URL,接下来就是定义每个Query需要取得的数据,根据页面定制接口数据。同时还可以做全局的异常处理,接口请求的合并。
接下来将讲一下我是如何安装轮子的,会涉及到一些面向场景的解耦操作,还有具体的代码演示。
对于我们应用的订单页面数据,在Restuful场景下首先会根据订单ID请求订单信息,接着依据从订单信息中得到的产品ID获取产品详细数据,之后还需要根据创建人ID获取客户详细数据,最后将这些数据结合起来才能渲染页面。
上图展示的是通过GraphQL来做订单页面时后端定义的一些类。最下方的员工类是一个基类,它包含了id、性别、部分、姓名这些通用的字段。
Employee对象被嵌套在Order类和Peuduct类中,在这两个类中能够很轻易的通过creator字段获取到Employee的数据信息。
而Order类和Peuduct类之间是相互引用的关系,通过items字段分别定义对应的对象数据。
这张图分别展示了前端Fragment和Query。前端Fragment是基于对象的,左边的第一段代码就是一个基于Employee对象的employee Fragment,我们可以从中获取到的id、gende、name等数据。在其他的Fragment中能够自由的引用employee对象。
Query其实就是GraphQL对传统前端Fragment的定义,它可以使用GraphQL官方提供的方法将关联的数据字段绑定给某个component。
在结合GraphQL和Apollo的情况下我们的鉴权方案主要依赖于AppAsyncStorage和web localStorage这两个数据可持久化的方案。
在登录过程中产生的token缓存到App AsyncStorage或web localStorage中,注册GraphQL Server的时候通过outLink来setContext,setContext方法主要用来重写header,在header中添加资源字段。这个资源字段一般是和后端商议后决定,不过Apollo官方的推荐通过传入token来实现整个鉴权方案。
如何使用轮子
前面提到过我们的终端设备中还包含树莓派,这是一个工业控制设备,一般被放置在用户的厂房中,用来打印记录库房数据。
我们通过阿里云物联网套件来实现服务器端和树莓派之间的通信,设备可以发布和订阅一些数据到MQTT中 ,每隔一段时间就会有心跳包从设备上传到MQTT,以此来更新页面数据。之后MQTT会将心跳包传递到消息队列中,然后再通过API到应用层来同步消息。
最后app端或web端通过暴露给他们的GraphQLAPI来读取到设备的信息,比如设备编号、固件版本、公网内网IP等。
当用户发现设备版本和服务器版本存在差异,执行需要更新的时候,本质上是发送动态操作到GraphQL API层,然后由GraphQL API层发送消息到设备 ,之后消息通过SDK API到达MQTT,接着发布一系列的请求到设备。最后设备重启完成之后会再重新发布心跳包到MQTT来更新一系列的操作。
使用轮子过程中的注意事项
在使用新轮子的过程中碰到的第一个问题就是在学习成本和团队适应方面。一是文档不够不够健全,相关的学习资料偏少,可能产品已经推出了2年时间,但是文档却是刚推出时写的。
除开文档问题之外,本质上还有一个思路的改变。原先使用Redux发送请求时,虽然和后端沟通麻烦了一点,但是毕竟已经和熟悉了。现在转换到GraphQL后,请求发起机制、数据刷新、文件上传等等都完全不同,相当于要从头开始学习新的东西。
在使用Apollo的过程中我们也遇到了一些坑。比如多次请求触发导致返回结果为underfined,之所以会这样是由于第一个接口请求发送出去后,还在loading阶段时,同一个接口又发送了第二次,导致返回数据发生冲突变成undefined。
还有资源对象和id重复导致资源数据被覆盖的问题,这是由Apollo的数据存储的特性所造成的,Apollo的每个资源对象的类型和id是定义数据字段唯一的标识。
以前设计的Component有容器组件和展示组件这两个概念,容器组件和Redux store之间存在交互,用来更新数据,展示组件则是单纯的展示数据。现在使用GraphQL之后,我们发现了一个更优的解决方案。
因为每个对象的资源字段固定,完全可以让每个Component和GraphQL 的Query片段一一对应。这意味着Component不再是为了请求而定义的,而是根据对象类型来定义
我们在使用GraphQL 的过程中曾出现过一次非常大的性能问题。当时为了更高效的开发,我们将每个资源对象全部使用Fragment来写,且每个对象都取到了所有页面中全部的数据。
此时一个要动态计算的字段被放在了一个基类中,在多个Fragment中循环调用,甚至嵌套调用。这时候后端就可能接收到一个需要计算n方次动态计算的结果的请求,服务器的负载压力可想而知。
以上这种情况对于前端来说,操作的只是某个实体的一个或几个资源字段。但是对于后端来说,每个实体背后可能对应着不同的数据库甚至不同类型的存储集群,同时后端集群间的海量数据自由join得到的结果。前端泛滥数据会导致后端负载急剧上升。
以上大概就是前端方面要讲的全部内容,接下请继续看下关于后端方面的问题。
使用新轮子的问题(后端)
使用GraphQL的过程中遇到的第一个比较严重的问题就是接口设计思路转变困难,之前在写RestfulAPI的时候想的更多的是面向资源,而GraphQL的设计思路则是面向场景,这完全颠覆了后端设计接口的哲学。
其次是开发feature时要更多考虑性能的问题,避免被查宕机。另外GraphQL相比Restful在鉴权模型设计上要投入更多的精力。
说完了这些弊端,再来看下有那些有利的地方。一是接口开发工作量大幅减少,重复性工作得以减少,避免了出错。其次是错误返回和文档等标准化工作可以借助工具本身完成。最后是对API多版本管理更加友好。
从后端来看早期的性能问题都是因为急于快速上线引起的,主要有以下几个因素。
第一点就是GraphQL的N+1场景,即前端在查询数据的时候可能首先要查到IDS数组,然后再map IDS数组重新对后端发起请求,最后后端通过多条SQL取到的可能是列表数据。
第二点是在前期开发的时候没有做请求层级限制,导致前端查询多层嵌套,服务器无法承受压力。
第三点是没有做分页的数目限制,一般来说前端可以通过传递特定的参数给接口来设置每页的接收数量,但是有些前端人员可能会为了快速上线新功能将每页的数量直接设置为9999,这样服务器就又会被搞宕机。
为避免发生一些安全问题,需要在认证、授权和请求频率限制几个方面多加注意。认证上每次都要在Header上加上token,认证机制应改为JWT,我们这边由于涉及到的终端较多,所以还会按平台检验合法性。
授权上可以针对操作也可以针对字段进行具体的授权。请求频率限制上既要防止恶意刷请求也要实现基于IP和UserID的限制。
这里的标题是安装工人的心得,其实指的就是我个人在使用GraphQL过程的一些感悟和总结。
先来看看GraphQL还有那些弊端。
第一,虽然后端已经做了一些优化,但是还是没有完全实现前端的按需查询,当数据量达到一定级别的时候,数据库查询可能会成为性能瓶颈。
第二,生态群并不完善,文档和实例缺乏,可能在google查询到的资料无法确切的解决问题。
第三,评价一个技术的实用性,不光要看技术力还要看它能否快速落地。一个现有的项目要从Restful切换到GraphQL,其实重构的压力很大,且重构和业务并行推进困难。
第四,由于目前国内使用GraphQL的团队不是很多,所以很难招聘到有经验的工程师,需要从零开始积累。
谈完了弊端再来说下个人的感悟,总结起来就是三个确实。确实提升了前后端的协作开发效率;确实降低了在前后端协作过程中的错误率;确实提高了接口复用度和开发效率。
个人认为用开发工具本身提高无论是前端、后端还是DevOps的工作效率是未来的发展趋势,我也相信GraphQL未来会支持更多的框架,解决更多的问题,并在性能和安全上有进一步提升。
关于我们
有数派是专业的纺织SaaS产品,具备工业(硬件)控制、样品管理、仓库存储、销售管理、设计辅助等功能,应用场景覆盖了行业几十个场景,近十个终端。
我们在技术栈的选择上,web用的是React,App用的React Native,后端主要的API源是用Ruby写的,还有部分Python用来做数据分析,目前所有的API都被迁移到了GraphQL上。硬件部分用到了C++,服务器的容器管理用的是Docker,DB则是PostgreSQL。
我们目前在终端上有树莓派、tv、pad、web、app、微信端、ipad。
以上为全部分享内容,谢谢大家!
IT大咖说 | 关于版权
感谢您对IT大咖说的热心支持!
相关推荐
推荐文章
点击【阅读原文】 打开干货通道
以上是关于面对极度复杂的前后端业务场景,使用 GraphQL 正确的姿势的主要内容,如果未能解决你的问题,请参考以下文章