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来做。

UNIONINTERFACE 是两种抽象类型,它们可以被当做普通对象一样,作为field 的返回值(output type)。但是因为它们是抽象的,因此在决定它们代表的真实对象的时候,需要借助额外的手段。

LISTNON_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 的名字叫 usersusers 接受一个参数 filer. 返回一个数组。(任何字段都可以添加0个或者多个 argument

因此服务端需要对 Query下的users 绑定一个解析函数。

注意,QueryUser 本质上是一样的,不同的是,GraphQL QueryMutationSubscription 是内置的。

在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的例子,当获取一批UserFriend,往往需要执行两个阶段,执行序列如下,

-- 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