一、中间层引发的思考
大家都知道我们目前应用的统一技术架构是前后端分离的形式,但是在实际开发中,两者之间在数据结构、数据交互方案、业务逻辑等等方面总是存在各种各样的矛盾,似乎一定要有一方在设计或实现上向另一方妥协。比如一个页面在初始化时,前端不得不调用多个接口来获取多个微服务提供的业务数据,因为后端希望在微服务之间减少互相调用达到彼此解耦的状态;再比如一个接口既有WEB端请求的需求也有APP端请求的需求,不可避免的就会出现一大堆冗余数据是我们不想看见的。
所以大家或多或少应该都思考过中间层的问题:也许我们可以把这些前后端之间的矛盾化解在中间层里。
在思考这个问题的时候,我们发现了一种全新的定义API的方式——GraphQL。传统的web API大概就是REST(表述性状态传递,Representational State Transfer),发送请求到一个特定的URL,然后你会收到各种结果,比如HTML,XML,JSON,PDF,JPEG…等等这些web可以理解的任一格式。但是GraphQl提出一种新的理念,如果前端开发可以自由定义请求和响应,如果前端开发可以精确地指定我要从API获取那些数据,是不是后端的接口设计只需要考虑业务模型做最正宗的RESTFUL API了呢?
这样理想化的只能出现在想象中的解决方案,无论是否能作为适合我们技术架构的中间层方案,似乎都值得我们深入了解一下。
二、GraphQL到底是什么
“一切皆是图:使用 GraphQL,你可以将你所有的业务建模为图”
这是官方的自身定义,其实GraphQL这个名字,Graph + Query Language,就表明了它的设计初衷是想要用类似图的方式表示数据。接下来会重点介绍一下GraphQL的“绘图”方式,请大家不要纠结于下面的语法,因为本文的目的不是让大家完全掌握GraphQL的语法,而是理解它的运行机制以及核心概念。
先来看一组常规的REST接口是如何设计的:
GET /api/v1/products/
GET /api/v1/product/:id/
POST /api/v1/product/
DELETE /api/v1/product/:id/
PATCH /api/v1/product/:id/
假设我们要在前端展示一个商品库,以上五个接口就是非常标准的请求地址定义,后台返回我们提前商议好的数据类型即可,那么GraphQL如何“绘制”这些接口呢?
query {
product(): [Product!]!
product(id: Int): Product!
}
mutation {
createProduct(): Product!
updateProduct(id: Int): Product!
deleteProduct(id: Int): Product!
}
第一次接触看起来有点陌生,好的,我们来解释两个概念:Schema 和 Tpye Modifier
1.Schema
Schema是用来描述接口获取数据的逻辑的,我们可以简单的理解为REST架构中每个独立资源的URI,只不过在GraphQL中,是通过<查询>来描述资源的获取方式,Schema就是多个<查询>组成的一张表。(本文把<查询>做了特殊标记,希望明确它是属于GraphQL的一种特殊行为,区别于其他语义)
上面的商品库案例中,两个特殊标记query和mutation就是<查询>的其中两种类型,也是我们在项目中最常用的两种,还有第三种类型是为了长链接提出的subscription类型。三种类型对应不同的<查询>场景:
- query(查询):当获取数据时,应选取Query类型
- mutation(更改):当尝试修改数据时,应使用mutation类型
- subscription(订阅):当希望数据变更时,可以向客户端进行消息推送时,使用subscription类型
很明显,获取商品库列表和单个商品详情属于query类型,新增、修改、删除商品属于mutation类型,我们好像读懂了一点上面的案例。第三种subscription类型是通过声明式的定义建立一个长链接,来获取服务端的推送消息,这里不展开。
2.Tpye Modifier
在GraphQL中有两种类型修饰符,分别是List和Required,他们在案例中体现分别是[Product]和Product!,[Product]表示它的类型是一个元素为Product的数组,Product!表示该字段为必须返回的字段。细心地小伙伴会发现案例中两者可以结合使用,那么关于 [Product]! 和 [Product!] 和 [Product!]! (注意 ! 的位置)的区别为:
- 列表本身为必填项,但其内部元素可以为空
- 列表本身可以为空,但是当列表存在时其内部元素为必填
- 列表本身和内部元素均为必填
了解了Schema 和 Tpye Modifier之后,再来看一下GraphQL对商品库几个接口的定义
query {
products(): [Product!]!
product(id: Int): Product!
}
mutation {
createProduct(): Product!
updateProduct(id: Int): Product!
deleteProduct(id: Int): Product!
}
有没有觉得清晰简洁一目了然呢?但是这只完成了一半的工作,别忘了我们真正要做的是自定义返回数据啊!上面的<查询>中,到底会返回什么样的数据呢?我们需要再了解两个新的概念:Resolver 和 Type。
3.Resolver (解析函数)和 Type
我们在<查询>中声明了products(): [Product!]!,那么这个<查询>对应的解析函数名必然叫products,这样才能对应起来,这个解析函数的声明过程如下:
Query {
products {
id
name
price
classify {
id
name
}
}
}
这里就应用了两种简单的Type,标量类型和对象类型,很明显id、name、price就属于标量类型,classify就属于对象类型,classify中的id和name属于标量类型,这些应该很好理解。
GraphQL在解析这段查询语句时会按如下步骤:
- 首先进行第一层解析,当前<查询>的类型是query,同时它的名字是products;之后会尝试使用products的解析函数获取解析数据,第一层解析完毕。
- 对第一层解析的返回值,进行第二层解析,当前products还包含四个子Query,分别是id、name、price和classify:id、name、price为标量类型,解析结束;classify为对象类型,尝试再次获取数据,第二层解析完毕。
- 对第二层解析的返回值,进行第三层解析,当前classify还包含一个id、 name,由于它们是标量类型,解析结束。
可以发现,GraphQL大体的解析流程就是遇到一个<查询>之后,尝试使用它的解析函数取值,之后再对返回值进行解析,这个过程是递归的,直到所解析的类型是标量类型为止,整个过程我们可以把它想象成一个很长的解析链。
以上只是对GraphQL的运行机制以及核心概念进行最简单的介绍,如果大家感兴趣的话,可以到官网学习更详细的API。
三、GraphQL能帮我们做什么
1.请求冗余和数据冗余:
文章开头提出的两种矛盾,就是请求和数据的冗余问题,都可以通过GraphQL解决。
2.灵活且强类型的Schema:
GraphQL的<查询>具有明确声明特性是它最显著地特点。查询和返回数据有个明确定义不只是为了与API和实现方面保持一致,同时可以作为接口校验的一种方式。
3.降低接口变动的维护成本:
REST中,一旦要改动API,不管是增删值域,改变值域范围,还是增减API数量,改变API url,都很容易变成伤筋动骨的行为。GraphQL就轻松多了。GraphQL的Service,API endpoint很可能就只有一个,根本不太会有改动URL path的情况。至始至终,数据的请求方都只需要说明自己需要什么内容,而不需要关心后端的任何表述和实现。数据提供方,比如server,只要提供的数据是请求方的母集,不论它们各自怎么变,都不需要因为对方牵一发而动全身。
4.提升应用程序性能:
服务器虽然同样需要调用与客户端相同的服务和REST API。但是大多数数据是在同一数据中心内的服务器之间流动。这些服务器到服务器之间的调用延迟非常低,而且带宽非常高,与浏览器的网络调用相比,性能提升是非常显著的。
目前GraphQL的应用已经日益广泛了
总的来说GraphQL最大的优势,就是它能够大大提高开发者的效率,而且最大化地简化了前端的数据层的复杂性,并且使前后端对数据的组织观点一致。只是真正要用的话,需要考虑迁移成本、性能、缓存等等太多方面的要求,希望有一天我们可以真正去思考如何应用GraphQL。
参考资料:
1.官方文档:https://graphql.cn/
2.网飞的实战经验:https://baijiahao.baidu.com/s...