一、Why GraphQL?

现在有一个需求:根据文章ID来查找对应的 User 信息。

假设数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Users:
{
"id": 1,
"fname": "Richie",
"age": 27,
"likes": 8
},
{
"id": 2,
"fname": "Betty",
"age": 20,
"likes": 205
},
{
"id" : 3,
"fname": "Joe",
"age": 28,
"likes": 10
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Posts:
{
id: 1,
userId: 2,
body: "Hello how are you?"
},
{
id: 1,
userId: 3,
body: "What's up?"
},
{
id: 1,
userId: 1,
body: "Let's learn GraphQL"
}

1、传统方式的局限性

常见的方案是:定义两个接口,分别根据user id和post id 获取对应的 userpost 信息。

1
2
> https://localhost:4000/users/:id
> https://localhost:4000/posts/:id

整个过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET https://localhost:4000/posts/1
Response:
{
id: 1,
userId: 2,
body: "Hello how are you?"
}
我们现在拿到文章id为2的userId, 然后调用另一个接口获取用户信息
GET https://localhost:4000/users/2
Response:
{
"id": 2,
"fname": "Betty",
"age": 20,
"likes": 205
}

整个过程查询了两次数据库。

还没完,假如前端页面只用到了fname信息呢?

有人说从接口数据里只取 fname 就好啦。 但是你想过没有,接口返回给我们的是用户的所有字段: id, age, likes等。这些字段会随着请求量的增大,会消耗网络带宽,给我们的服务器带来压力。

又有人会说,那我重新定义一个接口专用于返回用户的 fname, 如:

1
https://localhost:4000/userByfname/:id

这样确实能避免我们上面提到的问题,但是会带来一些新的问题: 第一,我们需要根据user的字段创建分别创建接口。第二,随着应用的变化,user的字段信息肯定会发生变化,我们需要同时维护好这些接口,给开发人员带来心智负担。

那么我们如果使用 GraphQL 会怎么样

2、GraphQL 的优势

GraphQL 中只需创建一个 query 语句:

1
2
3
4
5
6
7
8
{
posts(id: 1) {
body
user {
fname
}
}
}

如果你仅仅只需用到 age 字段,你可以修改 query 语句:

1
2
3
4
5
6
7
8
{
posts(id: 1) {
body
user {
age
}
}
}

只需保证 user 字段在 GraphQLschema 中都提前定义好就行。

GraphQL 最大的优势就是既不需要新建接口,也无需与后端服务多次通信,就能精确的获取到我们想要的字段信息

二、GraphQL 基础入门

先从最基本的 GraphQL 操作类型开始:

1、Query

顾名思义,Query用于查询数据。是以字符串的形式通过Post请求中body体中传给服务器。也就是所有的GraphQL类型都是post请求。

以下Query用于从数据库中取出所有 users 的 fname 和 age信息:

1
2
3
4
5
6
query {
users {
fname
age
}
}

理想情况下,服务器应返回的数据格式是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
data : {
users [
{
"fname": "Joe",
"age": 23
},
{
"fname": "Betty",
"age": 29
}
]
}

如果成功,会返回一个含有key 为 data的JSON对象;如果失败,返回的key就是error; 我们可以根据这个来处理错误。

2、Mutation

Mutation 用于向数据库写入数据。可以把它想象为 rest 中的 post 和 put请求:

1
2
3
4
5
mutation createUser{
addUser(fname: "Richie", age: 22) {
id
}
}

定义了一个名为 createUser 的 mutation, 根据传入的fname和age向数据库添加一个user; 同时定义了id作为返回值:

1
2
3
data : {
addUser : "a36e4h"
}

3、Subscription

subscription 赋予客户端实时监听数据变化的能力,利用的 websockets 来实现的。

1
2
3
4
5
6
subscription listenLikes {
listenLikes {
fname
likes
}
}

上面定义了一个监听 usersfnamelikes 两个字段,一旦其中任何一个字段发生变化,都会以下面的形式通知客户端:

1
2
3
4
5
6
data: {
listenLikes: {
"fname": "Richie",
"likes": 245
}
}

假如有一个页面需要实时显示用户 likes 字段的变化,这个特性就变得非常有用。

以上就是 GraphQL 中最基本的操作类型, 虽然介绍的很简单,但是已经足够了。下面我就利用上面介绍的知识设计并且实现一个GraphQL 案例,来直观感受 GraphQL 的强大。

三、GraphQL 实战

首先创建一个 GraphQL 服务来分别响应 query, mutationsubscriptions 这三种操作。

1、初始化工程

1
2
3
> mkdir graphql-server-demo
> cd graphql-server-demo
> npm init -y

安装依赖:

1
> npm install apollo-server graphql nodemon --save

引入 apollo-server 是因为它是一个开源的且提供强大能力的 GraphQL 服务器。

在package.json中的scripts加入以下脚本:

1
"scripts": { "start": "nodemon -e js, json, graphql" }

2、定义数据

在这个例子中,我们将数据定义为一个JSON对象。

在真实项目中通常是使用数据库来存放数据。

我们把数据定义在index.js中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const users = [
{
id: 1,
fname: 'Richie',
age: 27,
likes: 0,
},
{
id: 2,
fname: 'Betty',
age: 20,
likes: 205,
},
{
id: 3,
fname: 'Joe',
age: 28,
likes: 10,
},
];

const posts = [
{
id: 1,
userId: 2,
body: "Hello how are you?"
},
{
id: 1,
userId: 3,
body: "What's up?"
},
{
id: 1,
userId: 1,
body: "Let's learn GraphQL"
},
]

现在准备工作已经就绪,让我们开始实现这个例子吧。

3、Schema

第一步是要设计我们的 Schema,它包含了两个相互依赖的对象:TypeDefsResolvers

3.1 TypeDefs

之前我们学会了GraphQL的操作类型。为了让服务器知道什么类型是可以识别处理的, 而TypeDefs的作用就是定义这些类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const typeDefs = gql`
type User {
id: Int
fname: String
age: Int
likes: Int
posts: [Post]
}
type Post {
id: Int
user: User
body: String
}
type Query {
users(id: Int!): User!
posts(id: Int!): Post!
}
type Mutation {
incrementLike(fname: String!) : [User!]
}
type Subscription {
listenLikes : [User]
}
`;

首先定义了一个User,包含了 id, fname, age, likes, posts 五个属性,同时规定了这些属性的类型要么是String要么是Int

在GraphQL中支持四种基本类型:String, Int, Float, Boolean
代表这个字段是非空。

另外我们还定义了Query, MutationSubscription 等操作.

  • 定义两个query, 其中users 接收一个id参数, 返回类型是User。 另一个query posts 也接收一个id参数,且返回类型是Post
1
2
3
4
type Query {
users(id: Int!): User!
posts(id: Int!): Post!
}
  • Mutation 名叫:incrementLike, 接收一个参数fname, 并且返回一个User数组。
1
2
3
type Mutation {
incrementLike(fname: String!) : [User!]
}
  • Subscription 名叫: listenLikes, 返回一个User数组。
1
2
3
type Subscription {
listenLikes : [User]
}

现在,我们只是定义了类型,服务器并不知道如何响应客户端请求,所以需要为我们定义的类型提供一些处理逻辑, 我们把这个叫做 Resolvers

3.2 Resolvers

让我们继续编写resolvers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const resolvers = {
Query: {
users(root, args) { return users.filter(user => user.id === args.id)[0] },
posts(root, args) { return posts.filter(post => post.id === args.id)[0] }
},

User: {
posts: (user) => {
return posts.filter(post => post.userId === user.id)
}
},

Post: {
user: (post) => {
return users.filter(user => user.id === post.userId)[0]
}
},

Mutation: {
incrementLike(parent, args) {
users.map((user) => {
if(user.fname === args.fname) user.likes++
return user
})
pubsub.publish('LIKES', {listenLikes: users});
return users
}
},

Subscription: {
listenLikes: {
subscribe: () => pubsub.asyncIterator(['LIKES'])
}
}
};

正如你所看到的,我们创建了6个resolvers函数:

1、users query 根据传入的id返回相对应的 User 对象。
2、posts query 根据传入的id返回对对应的 Post 对象。
3、因为User TypeDefs中包含一个 posts 字段,所以需指定一个resolver来根据user id匹配到该用户发布过的文章。
4、同上,因为Post TypeDefs中包含一个 user 字段,所以需要指定一个resolver根据userId匹配到文章的作者。
5、incrementLike 作为一个变更操作,用于更新用户的 likes 字段。并将更新后的数据通过消息订阅发布模式发送给LIKES事件。
6、listenLikes 作为一个订阅对象,用于监听LIKES事件,并对此作出响应。

什么是订阅发布?
它是由GraphQL实现的基于websocket的实时通讯系统,websocket将这一切变得非常简单。

现在我们已经定义好typedefs和resolver,那么我们的服务也就可以正常🚀了!

startup

打开浏览器,并输入http://localhost:4000/ 回车:

playground

Apollo GraphQL 提供了一个可交互的web应用,帮助我们测试query和mutations操作的结果:

testresult

至此,我们完成了一个简单的 GraphQL 服务器,并且能响应 Query , MutationSubscription 这些请求操作。😁是不是感觉很酷呢?

四、总结

GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。

GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。

引用某位大神曾经说过的话:

GraphQL将会取代REST

让我们拭目以待。