GraphQL 允许Client来 定制Server 的返回结果。传统的基于Restful的API或者rpc都面临着一个问题,就是Client 没有办法来决定Server返回什么数据。所有的数据的计算过程都是确定的,Client 只需要Send 一个req, 服务端接受到这个请求进行 response. Client并没有一种语言或者约束规则来影响服务端的执行序列。因此,一个简单的场景,
与REST的差异
- 基于REST的架构产生的请求如下
GET /user/1
将返回如下的对象
User{
id
name
avatar
sex
age
}
当Client 仅仅需要一个name 的时候,Client 唯一做的,就是获取到完整的 User
对象,然后在浏览器端 进行过滤。
- 基于GraphQL 来实现一个相同的功能(通过
HTTP POST
)
query{
user{
name
}
}
Server 端将返回
{
"user":{
"name":"Leslie"
}
}
类型系统
GraphQL 定义一个Type System. Type 定义了数据结构与数据约束。
这是的GraphQL成为一种应用层的协议。 通过Type 我们可以定义一个Schema。
Schema 就像是基于REST我们定义的API 信息,如请求参数/返回值。 Schema描述了 Type System 包含的全部信息:
所有的查询,都在一个
Query
的特殊类型下。所有的变更与修改,都在一个
Mutation
下。同时还有一个
Subscription
用来实现服务端的推送。
一个 简单的Schema 如下
# user
type User{
id:ID
name:String!
avatar:URL
sex:Int
age:Int
}
type Mutation{
changeUser(name:String):User!
}
type Query{
findUser(name:String):User!
}
这是一种SDL,描述了 这个Schema包含的查询与变更。
类型
GraphQL 系统支持很多类型
Scalar
- ID
- Int
- String
- …
Object
Input
Enum
Union
Interface
LIST
NON_NULL
注意,Scalar 是基本的数据类型,所有的复杂类型,如 Object和Input,都是由于Scalar构成。
Scalar 本身定义的数据 是通过字面量 来传递到Server, GraphQL Server 有能力将一个字符序列通过序列化转换成合适的可以被网络传递的数据流(序列化)。当需要响应指定类型的数据,通过从数据流加载数据并转换成字面量。
因此服务端可以实现自己的Scalar类型。比如一个 UUID
的Scalar。
GraphQL spec 定义了ID
作为资源的唯一标识。 ID
在有些时候,跟数据库中的auto increment id 是一样的(比如是long
)类型,但并不总是如此。例如有些时候是uuid
.
GraphQL在某些时候,比如需要缓存的时候,可以根据ID
来做。
UNION
和 INTERFACE
是两种抽象类型,它们可以被当做普通对象一样,作为field 的返回值(output type)。但是因为它们是抽象的,因此在决定它们代表的真实对象的时候,需要借助额外的手段。
LIST
和 NON_NULL
可以看做是两个 wrapped type. 它们作用在其他Type 上,用来修饰类型本身。
在GraphQL 中,我们看到对一个对象的描述,往往是这样的:
users:[User!]!
这代表 这个 users
字段 是一个数组,并且不能为空,数组中的每一个元素是一个User
对象,这个User
对象本身也不能为空。
这样的修饰在 字段解析的时候是有含义的。它明确了字段必要的语义。并且增加了强约束。 比如spec 规定了,如果一个非空字段没有被返回,那么它的原因将出现在errors中。
INPUT
本身是一个Object. 拥有字段。跟Object
不一样的是,INPUT
是离散的,业务无关的,仅仅用作输入的字段的组合。Object
是业务内聚的,比如一个User
对象,不能包含 一个字段叫 weather
(实际上你也可以这样做,但并不是一个非常好的选择)。 如果没有INPUT
类型,当我们将一个User
对象作为输入,那么如果要扩展 输入源,就必然要修改User
本身。这样,系统的熵将越来越大。
interception
GraphQL
可以自省,这代表着通过query
我们可以观察到 graphQL 的schema结构。
结果包含了所有的type与备注,例如graphiql
就是基于此来产生一个可阅读文档。
Resolver/DataFetcher
GraphQL 的核心就是通过Type构建一个Schema,来描述一张graph
。并且对字段绑定resolver
函数。(有些时候叫 dataFetcher
)。
如 下面的例子
query{
users(filer:String!):[User]
}
enum SEX{
M
FM
}
type User{
idCard:ID!
name:String!
age:int
sex:SEX
}
在 query
下 有 field
的名字叫 users
, users
接受一个参数 filer
. 返回一个数组。(任何字段都可以添加0个或者多个 argument
)
因此服务端需要对 Query
下的users
绑定一个解析函数。
注意,Query
和 User
本质上是一样的,不同的是,GraphQL Query
、Mutation
、Subscription
是内置的。
在GraphQL的一个schema
中, Type是唯一的,因此你无法定义两个 User
。 所以,每一个字段的解析函数都可以通过 Type.field
来确定。这个就是字段坐标。
比如在我们的例子中,可以采用Query.users
来确定users
的值.
对于一个GraphQL 中的任意字段都可以绑定一个解析函数,
比如当我们的User
对象 包含一个Friend
数组的时候,
type Friend{
id:String!
touchTime:Date!
}
type User{
idCard:ID!
name:String!
age:int
sex:SEX
friends:[Friend!]
}
我们可以单独给 friends 绑定 绑定字段解析函数
User.friends=()-> parseFromSomewhere()
注意,当给Query.users
绑定一个解析函数的时候,解析函数返回的值如果包含
[{
"idCard":"",
"name":"Leslie",
"age":19,
"sex":"M"
}]
GraphQL 将会自动映射到对应的字段上,无需为每一个User
字段都绑定解析函数。
还有一种情况是,当我们为 friends
绑定一个函数解析的时候,实际上我们期望获取到是
当前user
的朋友,因此我们在绑定 User.friends
的时候,其实依赖的是User
产生的上下文。比如 idCard
, 因此在配置resolver
的时候,可以通过source
来获取 Query.users
返回的idCard
作为 User.friends
函数的上下文。
如
loadFriendById($idCard)
N+1
上面的users
的例子,当获取一批User
的Friend
,往往需要执行两个阶段,执行序列如下,
-- frist
`getUsers` 获取一个数组的list
---- And more
loadFriendById($idCard_1)
loadFriendById($idCard_2)
loadFriendById($idCard_3)
loadFriendById($idCard_4)
loadFriendById($idCard_5)
loadFriendById($idCard_N)
N+1 的问题的解决方案一般是采用 dataloader
。具体的原理是 在执行loadFriendById
的时候,并不立即返回结果,而是返回一个promise
, 将请求汇总在一起批量提交。
如 loadFriendsByIds($idCards)
这样可以极大减少网络IO开销。
分页
我们常常采用的分页是 page
& size
,通过page
+size
来定位到 offset
,在进行偏移来获取数据。 Relay
framework 提供了一种基于 cursor
的分页最佳实践。
可以参考这里 Relay Style Connection