在前几天的《StateOfJS: 2018年JavaScript生态圈趋势报告》一文中,我们看到了2018年在数据层GraphQL的发展势头猛烈,并且大部分用户用过都说好,但如上图数据显示,目前国内的使用人数还很少,大部分人连听都没听过,今天小肆就为大家介绍一下,何为GraphQL。
一. GraphQL为何会出现?
当提起API设计的时候,大家通常会想到SOAP,RESTful等设计方式,从2000年RESTful的理论被提出的时候,在业界引起了很大反响,因为这种设计理念更易于用户的使用,所以便很快的被大家所接受。我们知道REST是一种从服务器公开数据的流行方式。
当REST的概念被提及出来时,客户端应用程序对数据的需求相对简单,而开发的速度并没有达到今天的水平。
因此REST对于许多应用程序来说是非常适合的。然而在业务越发复杂,客户对系统的扩展性有了更高的要求时,API环境发生了巨大的变化。特别是从下面三个方面在挑战api设计的方式:
1. 移动端用户的爆发式增长需要更高效的数据加载
Facebook开发GraphQL的最初原因是移动用户的增加、低功耗设备和松散的网络。GraphQL最小化了需要网络传输的数据量,从而极大地改善了在这些条件下运行的应用程序。
2. 各种不同的前端框架和平台
前端框架和平台运行客户端应用程序的异构环境使得我们在构建和维护一个符合所有需求的API变得困难,使用GraphQL每个客户机都可以精确地访问它需要的数据。
3. 在不同前端框架,不同平台下想要加快产品快速开发变的越来越难
持续部署已经成为许多公司的标准,快速的迭代和频繁的产品更新是必不可少的。对于REST api,服务器公开数据的方式常常需要修改,以满足客户端的特定需求和设计更改。这阻碍了快速开发实践和产品迭代。
二. GraphQL官方定义:一种用于 API 的查询语言
GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。
请求你所要的数据:不多不少
向你的 API 发出一个 GraphQL 请求就能准确获得你想要的数据,不多不少。 GraphQL 查询总是返回可预测的结果。使用 GraphQL 的应用可以工作得又快又稳,因为控制数据的是应用,而不是服务器。
获取多个资源:只用一个请求
GraphQL 查询不仅能够获得资源的属性,还能沿着资源间引用进一步查询。典型的 REST API 请求多个资源时得载入多个 URL,而 GraphQL 可以通过一次请求就获取你应用所需的所有数据。这样一来,即使是比较慢的移动网络连接下,使用 GraphQL 的应用也能表现得足够迅速。
描述所有的可能:类型系统
GraphQL API 基于类型和字段的方式进行组织,而非入口端点。你可以通过一个单一入口端点得到你所有的数据能力。GraphQL 使用类型来保证应用只请求可能的数据,还提供了清晰的辅助性错误信息。应用可以使用类型,而避免编写手动解析代码。
三. GraphQL和RESTful的区别
前面提到GraphQL可以理解为基于RESTful的一种封装,目的在于构建使Client更加易用的服务,可以说GraphQL是更好的RESTful设计。
在过去的十多年中,REST已经成为设计web api的标准(虽然只是一个模糊的标准)。它提供了一些很棒的想法,比如无状态服务器和结构化的资源访问。
然而REST api表现得过于僵化,无法跟上访问它们的客户的快速变化的需求。 GraphQL的开发是为了应付更多的灵活性和效率,它解决了与REST api交互时开发人员所经历的许多缺点和低效之处。
为了说明在从API获取数据时REST和GraphQL之间的主要区别,让我们考虑一个简单的示例场景:在blog应用程序中,应用程序需要显示特定用户的文章的标题。同一屏幕还显示该用户最后3个关注者的名称。
REST和GraphQL如何解决这种情况?
使用REST API来现实时,我们通常可以通过访问多次请求来收集数据。
比如在这个示例中,我们可以通过下面的三步来实现:
-
通过
/user/<id>
获取初始用户数据 -
通过
/user/<id>/posts
返回用户的所有帖子 -
请求
/user/<id>/followers
返回每个用户的关注者列表
调用关系如下图所示:
如果用GraphQL的话,我们只需要一次请求就可以完成上述的需求
在GraphQL的世界里我们不用多取数据,也不用担心数据取少了,我们只需要按需获取即可。
REST最常见的问题之一是API的返回数据过多或者过少,这是因为客户端下载数据的唯一方法是通过访问返回固定数据结构的endpoint,这就会导致我们设计API非常困难,因为它既要能够为客户提供精确的数据需求,又需要满足不同调用者的需求,这本身就是相互矛盾的。GraphQL的发明者Lee Byron提出了一个很重要的概念: “用图形来思考,而不是endpoint”
通过上述直观展示我们可以得出一下几点:
1. 获取了许多多余的数据
通常情况下我们在调用一个通用API接口时,客户端获取的信息比应用程序中实际需要的要多。例如UI需要显示一个用户列表,而实际上只需要使用他们的名字。在REST API中通常会调用 /user 这个endpoint,并接收一个带有用户数据的JSON数组。但是这个响应可能包含更多关于返回的用户的信息,例如他们的生日或地址,而这些信息对客户来说是无用的,因为它只需要显示用户的名字。
2. 获取的数据少于Client所需要的数据
一般来说数据获取不足意味着某个特定的endpoint没有提供客户端需要的足够信息,客户端将需要额外的请求来获取它所需要的一切。这可能会升级到客户端需要首先获取列表信息,然后需要对单条数据添加一个额外的请求以获取其他所需的数据。
3. 前端的快速产品迭代对API有很大的挑战
REST api的一个常见模式是根据您在应用程序内部的展现逻辑来构造endpoint,这很方便,因为它允许客户端通过访问相应的endpoint获取特定视图的所有所需信息。
这种方法的主要缺点是它不允许前端的快速迭代。对于UI所做的每一个更改,现在都存在比以前更多(或更少)的数据的高风险。
因此,需要对后端进行调整,以满足新的数据需求,这会降低生产力并显著降低将用户反馈集成到产品中的能力。 使用GraphQL这个问题就解决了。
由于GraphQL的灵活性,无需在服务器上额外工作就可以在客户端上进行更改。由于客户端可以指定准确的数据需求,所以当前端的设计和数据需求发生变化时,并不需要后端API做出任何的修改就可以满足展现层的变化。
4. Schema和类型系统的好处
GraphQL使用强大的Type System来定义API的功能。所有在API中公开的类型都是使用GraphQL schema Definition Language (SDL)在模式中编写的。
该模式充当客户端和服务器之间的契约,以定义客户机如何访问数据。 一旦定义了模式,在前端和后端工作的团队就可以在没有进一步通信的情况下完成工作,因为他们都知道通过网络发送的数据的确切结构。
前端团队可以通过mock所需的数据结构来轻松测试他们的应用程序。一旦后端API实现完成,就可以对客户端应用程序进行切换来调用实际的API获取数据,这也可以使得我们实现更好的客户端和服务端的分离。
四. GraphQL语法
基础语法
其实GraphQL所需要学习的语法很少,大部分语法与我们平时的语法一致,可以通过官网详细了解。
首先,GraphQL是一门强类型语言,所以和我们在数据库定义一张表一样,我们需要定义每一个属性的类型.如下图所示:
下面是一个简单的类型定义,先是定义了一个枚举,然后我们定义了一个类型,类型中有四个属性:id、 name、 friends、 appearsIn,其中id和name是标量类型,而friends是一个Person类型,这是一个嵌套类型,仔细想想应该没什么毛病,毕竟你的朋友和你一样,都是人,而appearsIn是一个枚举类型,看起来还是很熟悉的。
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
type Person{
id: ID!
name: String!
friends: [Person]
appearsIn: [Episode]!
}
了解完类型,再了解一下Arguments和resolver,两者都是偏服务端一些,但是了解一下,对graphql的使用原理有进一步的认识。
对于一个Restful API来讲,除了知道接口URL,我们还需要知道接口的传参定义,对于GraphQL其实也一样,虽然URL只有一个,不同的接口通过type来区别,但传参同Restful API一样,体现了客户端与服务端的交互。
比如下面,查询的目标是id = 2的用户,获取他的用户名:
Query{
user(id: 2) {
id
userName
}
}
而在服务端定义一个接口时,我们也需要去定义入参,主要从两个方面,一是类型,二是其是否必填,比如下面这样:
接口定义
user: {
type: UserType,
args: {
id: { type: GraphQLID }
},
resolve: (root, args, context, info) => {
const { id } = args;
return getUser(id);
}
}
查询定义
上面的代码只是定义了一个输入属性id,并未定义其是否是必填,所以当查询时,如果没有配置查询id,查询不会报错,只会获取一个为null的空值结果。但是讲道理的话,我们希望这是一个必填项,所以我们需要修改服务端的代码,将id: { type: GraphQLID }
更换为id: {type: new GraphQLNonNull(GraphQLID)}
,这句代码的含义就表示id是一个类型为ID的必填项,再次执行我们的查询可以得到下面的错误提示,提示id是一个必填的ID类型,同时右侧也没有获取到为空的查询结果.
在讲上面Arguments时候,可以零星的看到type中有一个resolve方法,其接收root, args, context, info四个参数。
其中root代表这个type上父节点的resolve值(因为GraphQL支持嵌套查询),args就是上面讲的,context表在Resolver解析链中不断传递的中间变量,和react的上下文相似,而info这个概念,是当前Query的AST对象,比较抽象,但是可以通过查看info,获取这个QUERY的编译对象。这个方法也是后端服务编写的重点部分,常常我们可以在这里与已有的Restful API关联起来。
核心概念
Schema可以说是GraphQL最具核心的部分,其描述了整个接口向外暴露的形式。
像Restful API,我们会定义一个查询所有人的接口url定义为:
/api/v1/user/getUsers
查询人具体信息的接口url为:
/api/v1/user/getUserById
新增一个人员的接口url定义为:
/api/v1/user/createUser
这样前端人员调用起来会很直观。
但是graphql是完全不一样的使用方式,其向前端暴露的url就一个像/api/graphql
之类的,那这么多接口怎么区分呢? 我们来看看:
奥妙就是上面这张图,一个graphql接口都有一个Schema定义。
其定义三种操作方式:query(查询),mutation(变更)和subscription(监听)。
再往下延伸,一个查询中包含多个field,也就是多种不同的查询,比如query user查询人,query message查询消息,query weather查询天气。
通过这些就实现了Restful API使用多个url来达到不同操作的效果。
总结:
今天我们只是讲了一些GraphQL的基本知识,但我们依然可以看出GraphQL的出现可以使我们后端API具有更大的灵活性以及扩展性,满足了不同client对数据的需要,大大丰富了API的数据提供的能力。