结合产品,对比Graphql与Restful API在同场景下的应用情况。 发布时间:2019-07-16 17:23:49 #### 前言: ``` 近期学习了Graphql相关的知识,同时在网上看到好多后端研发人员都提出类似“使用成本高”,甚至还开起来玩笑说到“简化了前端却不把后端当人使”的看法,由于作为一个可以独立研发的人,我有自己本身对Graphql在实际项目使用场景的看法。 因为知乎和简书上有大量的Graphql的原理以及字段定义、使用方式等等,所以本篇文章略过对Graphql的原理和使用普及,将会以实际产品应用场景进行对Graphql的更深一步了解。 详情可参考GITHUB V4版本API为什么会采用Graphql这篇文档 https://developer.github.com/v4/ ``` ##### Graphql、Restful API区别: 1. Graphql是单一端点的API设计模式,可以由前端或客户端提供不同的Json语句组合,用过Schema的校验和执行生成对应前端、客户端所需要的接口数据; 2. Restful API是多端点,一个API对应一个数据返回。 ##### Graphql、Restful API在特殊内容需对多接口合并的处理: 1. Graphql默认就是可以让前端、客户端根据自己的内容需求定制Json语句,然后直接通过一个请求获取到原本需要通过请求多个Restful API才能获取到的内容组合; 2. Restful API接口是一对一,前端、客户端需要多次请求HTTP 接口获取到数据和进行组合,又或者是后端从多个不同Database或者不同人写的数据方法来进行合并数据进行输出。 <br/> ------------ <br/> #### 主要的2个点介绍完,那直接进入实例主题,为方便本司同事的理解将以我们本身的产品作为示范(由于大家都熟悉Restful API,所以我就先讲它): <br/> #### 我们先看下图页面(图A)  <p align="center">(图A)</p> <br/> ------------ <br/> #### Restful API篇 如上图(图A)所示,我们已知数据源,同时确定页面需要用到数据源中的“基础信息”、“权限”、“恶意行为”、“漏洞检测”、“历史版本”、“域名”、“IP”、“手机”,然后**根据高负载WEB的设计要求,页面需要尽量减少HTTP请求的原则(这里是重点之一,下面的示例API合并皆与它有关)**,我们可以把该页面所需要数据设计为如下(图B)三个后端接口进行异步加载(我们把这类接口定义为多数据合一的定制接口): <br/>  <p align="center">(图B)</p> <br/> 如果三个接口负载都不高的话,又或者是更粗暴一些,直接将API1、API2、API3三个数据合在一起当成一个接口输出(实际上,我们目前的情况就是很粗暴的合3为1 =.=)。 <br/> #### 接上面的情况我们进入新环节 我们通常都会遇到的情况,随着产品的运营不管是To B还是To C类都会遇到产品功能和内容的调整。 现在以图B为主,首先我们有后端A、B、C三个研发进行数据源的提供,那么我进行一个这样的假设,假设我是做To B类的产品,甲方一的要求如图B一样那么我们可以保持接口不更改,但是现在有个甲方二和甲方三有需求进行如下调整: ###### 甲方二: 1. 在API1里头加入“应用类名”; 2. 在API3里头删除“应用历史版本”,同时新增一个“命中规则”; 3. 有个新的页面,需要用到“应用基础信息”、“应用恶意行为”、“应用漏洞检测”、“命中规则”、“背景信息”,为此我们需要提供一个新的API4。 ###### 甲方三: 1. 在API1里头需要新增一个“仿冒应用识别”的功能,同时需要把API2的数据合并到API1; 2. API3删除历史版本,同时要加入“应用类名”、“应用方法名”、“应用文件结构”。 <br/> **由于甲方A、B、C三者的不一致性导致了会出现的情况:** 1. 维护多个不同后端API的版本(无限增长),后续的版本和功能完善与之前的定制化版本合并十分麻烦; 2. 随着页面的增长,部分接口会导致后端A、后端B、后端C的数据源大量交叉,有些数据源需要重复写接口。*(如有2个新的页面,第一个页面需要的“应用基础信息”+“应用域名”+“应用IP”+“恶意行为”数据产生API5;第二个页面需要“应用基础信息”+“命中规则”+“漏洞检测”+“恶意行为”数据会产生API6,其中“应用基础信息”、“恶意行为”就属于交叉数据);* 3. 由于接口交叉严重以及人员变动所带来的维护成本(实际上我们就遇到过好几次这情况了,例如某个API出了问题,得从不同的后端那里调试DEBUG才可以排查到是谁的数据源出了问题),甚至还不可避免的前后端一块推倒部分旧接口重新做新接口。 <br/> #### 我作为一个有经验的Restful api开发者对上面提的问题有很大的疑问: **Q:上面说提到的产生的三个问题,我用Restful api严格按照细分模块化的形式也能很好解决!** A:嗯,我们说到Restful api细化处理确实可以解决这个问题,但是同时没有逃过另外一个很关键的问题,**“一个高性能的WEB、客户端应用应该尽量减少不必要的HTTP请求”**,为什么之前会出现前端的多ICON合一以坐标形式获取?为什么要压缩前端CSS、JS代码打包在单个文件?这所有的根源都是**“避免过多的HTTP请求”**,细化后的RestFul api确实可以避免上述的问题,但是造成的就是前端、客户端需要调用很多的请求才可以获取到想要的数据,其中里面还有异步获取同级数据时,需要交叉组合先后循序的逻辑判定。 PS:Restful api模块化细分这种方式早在GITHUB API V3版本就使用了,为什么后面GITHUB API要单独推出v4 Graphql版本?GITHUB直接表示,接口细化处理后,好大部分用户要显示某些数据时,需要通过多次的HTTP发起请求才能组合成他们想要的数据,而且每个用户的组合模式还有差异化,V3版本的Restful API没办法满足他们差异化需求,所以采用了V4版本的API直接采用了Graphql来解决这个问题。 关于GITHUB为什么要采用Graphql的参考文章:https://github.blog/2016-09-14-the-github-graphql-api/ <br/> ------------ <br/> #### Graphql篇 上面的同一情况我们放到Graphql又会如何?我们先直接看下图C。  <p align="center">(图C)</p> 首先我们要做的是,不同后端A、后端B、后端C根据性能优化(不管是缓存还是集群啥优化的)加载Graphql插件并写每一个数据源的schema和resolve处理,然后等前端根据页面需求去定制Json1、Json2、Json3返回给页面使用,再然后呢..然后..然后就是看产品和前端折腾就行^.^b!! ```javascript /** * 这里简单演示下获取前面图B数据的方式,前端直接根据产品页面内容写语句(即图C里头的Json1,可以说现在已经跟后端无关系了) */ { base(sha1: '变量1'){}, permission(sha1: '变量1'){}, Security(sha1: '变量1'){}, Malware(sha1: '变量1'){}, History(sha1: '变量1', page: '变量2'){}, Domain(sha1: '变量1'){}, IP(sha1: '变量1'){}, Phone(sha1: '变量1'){} } /** * 如果这里正好遇到了我前面说过的个别接口有高负载的情况,那么我们可以分两次提交Json进行异步获取数据,十分方便敏捷 * 假设Malware、Security两个数据源都比较慢 */ { base(sha1: '变量1'){}, permission(sha1: '变量1'){}, History(sha1: '变量1', page: '变量2'){}, Domain(sha1: '变量1'){}, IP(sha1: '变量1'){}, Phone(sha1: '变量1'){} } { permission(sha1: '变量1'){}, Security(sha1: '变量1'){} } /** * GraphQL会根据前端提交过来的Json格式去拿对应的数据源数据返回给前端。 * 另外,每个KEY对应的 {里头可以写你想要的字段或者不写、也可以写高级的联动语法,可以直接A数据源联动B数据源,这里不演示了,大伙可以自行看文档。} */ ``` <br/> #### 接下来直接进入我们上面Restful api所遇到的问题新环节 在同样的问题下,看看换成Graphql后是怎么样的? 同等情况下遇到产品新需求又或者是甲方二和甲方三的情况要怎么处理?看过上面的代码演示后,**我们都清楚知道了只要简单更改一下前端提交过去的JSON就可以获得不一样的数据结果,复用度和可定制度十分的高,而且再也不用后端去重新书写新的接口,又或者是针对不同的甲方而要维护过多的后端接口版本代码!** 根据上面的情况我直接演示下甲方二、甲方三需求提过来时的实际情况变化: ```javascript /** * 根据甲方二提的3个需求,我们前端只需要做如下更改 */ //页面1需要修改如下 { base(sha1: '变量1'){}, Class(sha1: '变量1'){}, //新增 permission(sha1: '变量1'){}, Security(sha1: '变量1'){}, Malware(sha1: '变量1'){}, //History(sha1: '变量1', page: '变量2'){}, 删除 Domain(sha1: '变量1'){}, IP(sha1: '变量1'){}, Phone(sha1: '变量1'){}, Rule(sha1: '变量1'){} //新增 } //新增页面 { base(sha1: '变量1'){}, Security(sha1: '变量1'){}, Malware(sha1: '变量1'){}, Rule(sha1: '变量1'){}, BackGround(sha1: '变量1'){} } //甲方三提的2个需求因为耦合性的关系,所以更简单,后端需要先增加一个“仿冒应用识别”的数据源,剩下的就是前端简单改下Json格式就好 { base(sha1: '变量1'){}, Class(sha1: '变量1'){}, permission(sha1: '变量1'){}, //History(sha1: '变量1', page: '变量2'){}, 根据甲方三需求删除 Domain(sha1: '变量1'){}, IP(sha1: '变量1'){}, Phone(sha1: '变量1'){}, fake(sha1: '变量1'){}, //根据甲方三需求新增 Function(sha1: '变量1'){}, //根据甲方三需求新增 File(sha1: '变量1'){} //根据甲方三需求新增 } ``` OK搞定,这个就是Graphql的处理方式和Restful API处理方式的区别,全程只有产品、甲方跟前端打交道!后端不需要参与!! 听说因为不同的甲方需要后端维护不同甲方的API版本??**不存在的^v^b!!没有接口这么一个说法!!** 听说随着页面的增长需要写好多交叉数据的接口??**不存在的^v^b!!根本不用写定制化接口!!** 听说人员变动各种接口维护成本十分巨大,出了问题要code review慢慢找??**不存在的^v^b!!根本就没接口这回事!!**况且如果某个数据源出错了,直接就可以定位到是谁的数据源,再也不用担心数据交叉导致需要找多个不同后端研发调试找问题。 <br/> #### 我们来一段现实应景场景模拟: **Restful api篇:** 产品:甲方一提了个新需求!需要加XX数据。 后端:好的我看了一下,需要从后端C和B那边再取数据合并才可以显示,我先去修改下这个接口。 前端:好的,接口好了后给我,因为是新增的接口,我需要重新写ajax http请求。 产品:甲方二提了个新需求!需要增加XXX页面。 后端:好的我看下设计图,大概需要XX和XX数据,我现在给前端出一个新增的XXX页面接口。 前端:好的,接口好了后给我,我去套模板数据。 产品:甲方一又提了个新需求!需要在XX页面新增不同系统的支持识别! 后端:。。。好的,我先去看一下以前甲方一的分支代码,然后再给你加上。 前端:好的我也看看以前甲方一的前端代码,好多接口可能要改一下,我得找到对应路由的URL字段进行修改。 产品:我们业务扩展了!!新增了甲方三、甲方四、甲方...、甲方二十的项目,但是里头有各自的定制! 后端:.........!!你滚!!! 前端:....后端我等你,但是你不要让我等你太久。。。 后端:#!¥%#¥……#¥%……¥%&%……*&%……&#%#¥% (PS:在好些新设计好页面,前端都必须等后端把页面所要的每一个新增的接口写好或者给demo数据才可以接,有多少个新接口就得写多少个http请求) <br/> **Graphql篇:** 产品:甲方一提了个新需求!需要加XX数据。 后端:好的,我去增加一个xx的数据源。 前端:好的,我去修改一个json传参,再加个模板渲染就OK。 产品:甲方二提了个新需求!需要增加XXX页面。 后端:66666。 前端:好的,我去根据设计图,去组合我想要的数据格式套进去就好。 产品:甲方一又提了个新需求!需要在XX页面新增不同系统的支持识别! 后端:66666。 前端:有数据源的话我直接加多一个传参就OK!小问题。 产品:我们业务扩展了!!新增了甲方三、甲方四、甲方...、甲方二十的项目,但是里头有各自的定制! 后端:66666666666666666666666666666666!!!我们太真屌!!如果有新增的数据源,我去加一个就好贼快。 前端:我们去根据设计图页面,自己组合想要的字段就可以了! (PS:前端大部分时候都不需要等后端出接口,因为好多源数据原本就有,自己随意组合随意拿就可以,跟后端没关系!!部分需要后端写的新数据源,等后端写好了直接前端参数加上就可以。) <br/> #### 总结: 通过上述的实例和应用场景,大家都对Graphql有一定的认识,在一个项目上,**只要数据源不更改的情况下,不管任意的页面变动和产品设计需求更改都不需要后端过多参与,甚至不用为新页面写接口**。之所以会有网友提出“使用成本高”、“简化了前端却不把后端当人使”这类,主要是因为在现有的项目基础上做修改(**那肯定工作量大啊!!不然GITHUB v3 API干嘛要跳到V4版本才使用Graphql而不在v3的时候同时支持这两种!!**),所以我个人比较倾向于新的项目里头可以尝试这类东西,这样的“使用成本”只剩下后端和前端对Graphql的语法熟悉和习惯(**难你个头啊!当时前端源生JS闭包写法直接跳VUE和REACT的时候有哪个不是过来人!!**); <br/> ------------ <br/> #### Graphql多接口合并疑问: **Q:我们都知道Graphql是多数据单一端点获取数据的方式,如果我是一个数据量级别很大的项目采用Graphql,一个接口获取那么多个合并数据不会出现负载问题么?** A: 我们完全可以在配置文件里头区别出有高、低负载的数据源接口,根据页面需求自由合并低负载接口,然后对于高负载的数据源单独进行GraphQL的多次异步获取数据即可。 **Q:使用Graphql会遇到N+1数据N次查看的问题,怎么办?** A:这完全可以在前端进行重组又或者是后端获取数据时避免在for循环时单个查库获取索引对应数据,尽量先获取到所有的key作为集合再进行in查询,如: ``` //应该直接获取主索引统一获取方式 user_id in ('1', '2', '3') //不应该for的时候逐条单一获取 user_id:'1' user_id:'2' user_id:'3' ``` <br/> Author by Soljen From Appscan.io 热门推荐 RPO攻击技术浅析 FLARE IDA Pro 脚本系列:简化IDA中的graphs 支付宝克隆攻击原理分析与复现 Windows 10 内核驱动漏洞利用系列 - 空指针解引用