GraphQL一种API查询语言。
这正是GraphQL的强大之处,引用官方文档的一句话:
ask exactly what you want.
我们在使用REST接口时,接口返回的数据格式、数据类型都是后端预先定义好的,如果返回的数据格式并不是调用者所期望的,作为前端的我们可以通过以下两种方式来解决问题:
一般如果是个人项目,改后端接口这种事情可以随意搞,但是如果是公司项目,改后端接口往往是一件比较敏感的事情,尤其是对于三端(web、andriod、ios)公用同一套后端接口的情况。大部分情况下,均是按第二种方式来解决问题的。
因此如果接口的返回值,可以通过某种手段,从静态变为动态,即调用者来声明接口返回什么数据,很大程度上可以进一步解耦前后端的关联。
在GraphQL中,我们通过预先定义一张Schema
和声明一些Type
来达到上面提及的效果,我们需要知道:
这么说可能比较抽象,我们一个一个来说明。
对于数据模型的抽象是通过Type来描述的,每一个Type有若干Field组成,每个Field又分别指向某个Type。
GraphQL的Type简单可以分为两种,一种叫做Scalar Type(标量类型)
,另一种叫做Object Type(对象类型)
。
GraphQL中的内建的标量包含,String
、Int
、Float
、Boolean
、Enum
,对于熟悉编程语言的人来说,这些都应该很好理解。
值得注意的是,GraphQL中可以通过Scalar
声明一个新的标量,比如:
DateTime
和ID
这两个标量分别代表日期格式和主键Upload
标量来代表要上传的文件总之,我们只需要记住,标量是GraphQL类型系统中最小的颗粒,关于它在GraphQL解析查询结果时,我们还会再提及它。
仅有标量是不够的抽象一些复杂的数据模型的,这时候我们需要使用对象类型,举个例子(先忽略语法,仅从字面上看):
type Article {
id: ID
text: String
isPublished: Boolean
}
上面的代码,就声明了一个Article
类型,它有3个Field,分别是ID
类型的id,String
类型的text和Boolean
类型的isPublished。
对于对象类型的Field的声明,我们一般使用标量,但是我们也可以使用另外一个对象类型,比如如果我们再声明一个新的User
类型,如下:
type User {
id: ID
name: String
}
这时我们就可以稍微的更改一下关于Article
类型的声明代码,如下:
type Article {
id: ID
text: String
isPublished: Boolean
author: User
}
Article
新增的author
的Field是User
类型, 代表这篇文章的作者。
总之,我们通过对象模型来构建GraphQL中关于一个数据模型的形状,同时还可以声明各个模型之间的内在关联(一对多、一对一或多对多)。
关于类型,还有一个较重要的概念,即类型修饰符,当前的类型修饰符有两种,分别是List
和Required
,它们的语法分别为[Type]
和Type!
, 同时这两者可以互相组合,比如[Type]!
或者[Type!]
或者[Type!]!
(请仔细看这里!
的位置),它们的含义分别为:
我们进一步来更改上面的例子,假如我们又声明了一个新的Comment
类型,如下:
type Comment {
id: ID!
desc: String,
author: User!
}
你会发现这里的ID
有一个!
,它代表这个Field是必填的,再来更新Article
对象,如下:
type Article {
id: ID!
text: String
isPublished: Boolean
author: User!
comments: [Comment!]
}
我们这里的作出的更改如下:
最终的Article
类型,就是GraphQL中关于文章这个数据模型,一个比较简单的类型声明。
现在我们开始介绍Schema
,我们之前简单描述了它的作用,即它是用来描述对于接口获取数据逻辑
的,但这样描述仍然是有些抽象的,我们其实不妨把它当做REST架构中每个独立资源的uri
来理解它,只不过在GraphQL中,我们用Query来描述资源的获取方式。因此,我们可以将Schema
理解为多个Query组成的一张表。
这里又涉及一个新的概念Query
,GraphQL中使用Query
来抽象数据的查询逻辑,当前标准下,有三种查询类型,分别是query(查询)、mutation(更改)和subscription(订阅)。
Note: 为了方便区分,Query
特指GraphQL中的查询(包含三种类型),query
指GraphQL中的查询类型(仅指查询类型)
上面所提及的3中基本查询类型是作为Root Query(根查询)
存在的,对于传统的CRUD项目,我们只需要前两种类型就足够了,第三种是针对当前日趋流行的real-time
应用提出的。
我们按照字面意思来理解它们就好,如下:
仍然以一个例子来说明。
首先,我们分别以REST和GraphQL的角度,以Article
为数据模型,编写一系列CRUD的接口,如下:
Rest 接口
GET /api/v1/articles/
GET /api/v1/article/:id/
POST /api/v1/article/
DELETE /api/v1/article/:id/
PATCH /api/v1/article/:id/
GraphQL Query
query {
articles(): [Article!]!
article(id: Int): Article!
}
mutation {
createArticle(): Article!
updateArticle(id: Int): Article!
deleteArticle(id: Int): Article!
}
对比我们较熟悉的REST的接口我们可以发现,GraphQL中是按根查询的类型来划分Query职能的,同时还会明确的声明每个Query所返回的数据类型,这里的关于类型的语法和上一章节中是一样的。需要注意的是,我们所声明的任何Query
都必须是Root Query
的子集,这和GraphQL内部的运行机制有关。
例子中我们仅仅声明了Query类型和Mutation类型,如果我们的应用中对于评论列表有real-time
的需求的话,在REST中,我们可能会直接通过长连接或者通过提供一些带验证的获取长连接url的接口,比如:
POST /api/v1/messages/
之后长连接会将新的数据推送给我们,在GraphQL中,我们则会以更加声明式的方式进行声明,如下
subscription {
updatedArticle() {
mutation
node {
comments: [Comment!]!
}
}
}
我们不必纠结于这里的语法,因为这篇文章的目的不是让你在30分钟内学会GraphQL的语法,而是理解的它的一些核心概念,比如这里,我们就声明了一个订阅Query,这个Query会在有新的Article被创建或者更新时,推送新的数据对象。当然,在实际运行中,其内部实现仍然是建立于长连接之上的,但是我们能够以更加声明式的方式来进行声明它。
如果我们仅仅在Schema中声明了若干Query,那么我们只进行了一半的工作,因为我们并没有提供相关Query所返回数据的逻辑。为了能够使GraphQL正常工作,我们还需要再了解一个核心概念,Resolver(解析函数)
。
GraphQL中,我们会有这样一个约定,Query和与之对应的Resolver是同名的,这样在GraphQL才能把它们对应起来,举个例子,比如关于articles(): [Article!]!
这个Query, 它的Resolver的名字必然叫做articles
。
在介绍Resolver之前,是时候从整体上了解下GraphQL的内部工作机制了,假设现在我们要对使用我们已经声明的articles
的Query,我们可能会写以下查询语句(同样暂时忽略语法):
Query {
articles {
id
author {
name
}
comments {
id
desc
author
}
}
}
GraphQL在解析这段查询语句时会按如下步骤(简略版):
Query
的Root Query
类型是query
,同时需要它的名字是articles
articles
的Resolver
获取解析数据,第一层解析完毕articles
还包含三个子Query
,分别是id
、author
和comments
User
的Resolver
获取数据,当前field解析完毕author
还包含一个Query
, name
,由于它是标量类型,解析结束我们可以发现,GraphQL大体的解析流程就是遇到一个Query之后,尝试使用它的Resolver取值,之后再对返回值进行解析,这个过程是递归的,直到所解析Field的类型是Scalar Type(标量类型)
为止。解析的整个过程我们可以把它想象成一个很长的Resolver Chain(解析链)。
这里对于GraphQL的解析过程只是很简单的概括,其内部运行机制远比这个复杂,当然这些对于使用者是黑盒的,我们只需要大概了解它的过程即可。
Resolver本身的声明在各个语言中是不一样的,因为它代表数据获取的具体逻辑。它的函数签名(以js为例子)如下:
function(parent, args, ctx, info) {
...
}
其中的参数的意义如下:
article(id: Int)
中的id
)值得注意的是,Resolver内部实现对于GraphQL完全是黑盒状态。这意味着Resolver如何返回数据、返回什么样的数据、从哪返回数据,完全取决于Resolver本身,基于这一点,在实际中,很多人往往把GraphQL作为一个中间层来使用,数据的获取通过Resolver来封装,内部数据获取的实现可能基于RPC、REST、WS、SQL等多种不同的方式。同时,基于这一点,当你在对一些未使用GraphQL的系统进行迁移时(比如REST),可以很好的进行增量式迁移。