graphql入门(一)

直接进入主题。

graphql 中间层服务的搭建

搭建一个简单的 graphql 中间层服务

选用框架:koa,首先打开 apollo-server-koa 的文档,按照文档说明先把服务起起来。

  • 创建一个空的项目,然后执行npm install apollo-server-koa graphql

  • 创建 main.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
    const Koa = require('koa');
    const { ApolloServer, gql } = require('apollo-server-koa');

    // Construct a schema, using GraphQL schema language
    const typeDefs = gql`
    type Query {
    hello: String
    }
    `;

    // Provide resolver functions for your schema fields
    const resolvers = {
    Query: {
    hello: () => 'Hello world!',
    },
    };

    const server = new ApolloServer({ typeDefs, resolvers });

    const app = new Koa();
    server.applyMiddleware({ app });

    app.listen({ port: 4000 }, () =>
    console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`),
    );
  • 执行node main.js 将项目启动

  • 浏览器中访问http://localhost:4000/graphql

  • 在 Playground 中左半边输入

    1
    2
    3
    query {
    hello
    }
  • 点击中间的运行按钮,可以看到右半边会出现请求的内容

现在呢,中间层很简单的一个服务就启动起来了。apollo-server 提供了可视化界面用来满足开发调试。其他一些具体的配置请参考Config,这不是我们这节课的重点,就不细说了。

完善一下项目

只有一个 hello 很明显不能满足我们工作的内容,那么我们假设一个场景,我们要做一个年级学生管理的系统,每个年纪有好几个班,每个班有好多个学生,那我们可以用一个 json 文件来模拟数据库。json 我也为你们准备好了:

1
{"grade":[{"id":1,"name":"一年级"},{"id":2,"name":"二年级"}],"class":[{"id":1,"name":"1班","gradeId":1},{"id":2,"name":"2班","gradeId":1},{"id":3,"name":"1班","gradeId":2},{"id":4,"name":"2班","gradeId":2}],"student":[{"id":1,"name":"小A","classId":1},{"id":2,"name":"小B","classId":1},{"id":3,"name":"小C","classId":2},{"id":4,"name":"小D","classId":2},{"id":5,"name":"小E","classId":3},{"id":6,"name":"小F","classId":3}]}

我们在项目中建一个 database 的文件夹,在文件夹中建一个 index.json,把上面的 json 拷贝到文件中,我们就有了一个简陋的数据库。我们可以看到我们库中有三张表,年级、班级和学生,每张表都对应几条数据。
接下来呢我们就要开始针对业务来开发接口了。我们首先看graphql 的官方文档,发现内容和细节都很多,那接下来你可以跟着我做,做完之后呢,你就知道文档中的内容你应该怎么运用了。
首先我们要知道querymutation,文档上叫他们两为“查询”和“变更”,意思就是说一个管读,一个管写,你所有的 api 都应该在这里登记一下,这样 graphql 才知道你要干啥。querymutation 你可以理解为是两颗树的主干,你可以给他添一些枝干。那一个接口是应该放在query还是放在mutation,是怎么区分的呢?其实是靠人为区分的,他没有一个规定来保证你放的位置是正确的,比如,刚刚的 hello 接口,我们把它放在 mutation 中,他仍然可以返回。

我们把 main.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
const Koa = require('koa');
const { ApolloServer, gql } = require('apollo-server-koa');

// Construct a schema, using GraphQL schema language
const typeDefs = gql`
type Query {
hello: String
}

type Mutation {
hello1: String
}
`;

// Provide resolver functions for your schema fields
const resolvers = {
Query: {
hello: () => 'Hello world!'
},
Mutation: {
hello1: () => 'Hello world!'
}
};

const server = new ApolloServer({ typeDefs, resolvers });

const app = new Koa();
server.applyMiddleware({ app });

app.listen({ port: 4000 }, () =>
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`)
);

我们请求

1
2
3
mutation {
hello1
}

依然可以返回结果,这个约定其实类似 GET 请求和 POST 请求的约定,靠大家自觉遵守。
接下来就是改造项目了,首先我们应该将typeDefsresolvers分离开来,不然项目后期难以维护,那我们在项目中新建一个 schema 文件夹,文件夹中建一个 typeDefs.js 文件和 resolvers.js 文件,
将 typeDefs 和 resolvers 分别写到各自文件中,写完后应该是这样

  • main.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const Koa = require('koa');
    const { ApolloServer, gql } = require('apollo-server-koa');

    // Construct a schema, using GraphQL schema language
    const { typeDefs } = require('./schema/typeDefs');

    // Provide resolver functions for your schema fields
    const { resolvers } = require('./schema/resolvers');

    const server = new ApolloServer({ typeDefs, resolvers });

    const app = new Koa();
    server.applyMiddleware({ app });

    app.listen({ port: 4000 }, () =>
    console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`)
    );
  • typeDefs.js

    1
    2
    3
    4
    5
    6
    7
    const { gql } = require('apollo-server-koa');

    module.exports.typeDefs = gql`
    type Query {
    hello: String
    }
    `;
  • resolvers.js

    1
    2
    3
    4
    5
    module.exports.resolvers = {
    Query: {
    hello: () => 'Hello world!'
    }
    };

    改造完后试试还能不能跑起来,跑起来之后我们就开始实现我们的业务。
    首先我们先看查询,我们需要一个查年级的接口,一个查询班级的接口和一个查询学生的接口,写完是这个样子,先跟着我写,然后我一一解释都是什么意思

  • typeDefs.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
    const { gql } = require('apollo-server-koa');

    module.exports.typeDefs = gql`
    type Query {
    grade(gradeId: Int!): Grade
    class(classId: Int!): Class
    student(studentId: Int!): Student
    }

    type Grade {
    id: Int
    name: String
    }

    type Class {
    id: Int
    name: String
    gradeId: Int
    }

    type Student {
    id: Int
    name: String
    classId: Int
    }
    `;
  • resolvers.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
    const database = require('../database');

    module.exports.resolvers = {
    Query: {
    grade: (root, { gradeId }) => {
    const grades = database.grade;
    for (const grade of grades) {
    if (grade.id === gradeId) {
    return grade;
    }
    }
    },
    class: (root, { classId }) => {
    const classes = database.class;
    for (const classItem of classes) {
    if (classItem.id === classId) {
    return classItem;
    }
    }
    },
    student: (root, { studentId }) => {
    const students = database.student;
    for (const student of students) {
    if (student.id === studentId) {
    return student;
    }
    }
    }
    }
    };

    接下来我们看看刚刚写的是啥,首先我们在 Query 的类型中添加了三个东西“grade”, “class”, “student”,我们刚刚说了 Query 可以理解为一棵树的主干,那添加的着三个东西就是在主干上添加了三个枝干,或者可以理解 Query 为一个大的 JSON,给它添加了三个 key,但是我们看和 JSON 有啥不一样,它可以传参数,grade 中我们把 gradeId 传了过去,并且规定必须是 Int 类型,后面的那个“!”代表必须传这个 gradeId,“:”后面的“Grade”意思就是,调这个方法返回的是这个类型的数据,但是这个类型是不知道的啊,所以在下面定义了这个类型长啥样子。也就是 type Grade。我这里举例的只是一两个简单的类型,具体其他的类型可以查询graphql 的官方文档。这样呢,我们就可以进行简单的单表查询了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
query {
grade(gradeId: 1) {
id
name
}
class(classId: 1) {
id
name
}
student(studentId: 1) {
id
name
}
}

但是呢如果我要查询一个年级下有几个班级,这个就查不到了,那我们怎么做呢?我们期望的是在年级的类型中增加一个数组来表示改年级下的班级,就像是这样:

1
2
3
4
5
type Grade {
id: Int
name: String
classes: [Class]
}

那我们就把 Grade 的类型先改成这样,我们执行查询

1
2
3
4
5
6
7
8
9
10
query {
grade(gradeId: 1) {
id
name
classes {
id
name
}
}
}

看查询的结果 classes 是 null,到这里同学们应该想到了我们应该修改 resolvers.js。是的,没错,resolvers.js 就是对 typeDefs 的实现。那么我们该怎么修改呢?可能有的同学会说,直接修改 resolvers 中的 grade 的实现,让他把 class 查出来一起返回。这样确实也能达到目的,但是却不是 graphql 想要的效果。让我们仔细观察一下 typeDefs,看到它里面所有的定义都是类型定义,我们在回想 Query 是树干,其他类型是枝干的伏笔,那我们是不是可以发现其实 “type Grade” 和 “type Query” 长得很像?
如果是下面这样子那是不是就看起来一摸一样了

1
2
3
4
5
6
7
type Query {
grade(gradeId: Int!): Grade
}

type Grade {
classes(gradeId: Int!): [Class]
}

那我们应该就知道 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
36
37
38
const database = require('../database');

module.exports.resolvers = {
Query: {
grade: (root, { gradeId }) => {
const grades = database.grade;
for (const grade of grades) {
if (grade.id === gradeId) {
return grade;
}
}
},
class: (root, { classId }) => {
const classes = database.class;
for (const classItem of classes) {
if (classItem.id === classId) {
return classItem;
}
}
},
student: (root, { studentId }) => {
const students = database.student;
for (const student of students) {
if (student.id === studentId) {
return student;
}
}
}
},
Grade: {
classes: (root, { gradeId }) => {
const classes = database.class.filter(
classItem => classItem.gradeId === gradeId
);
return classes;
}
}
};

那我们调用的时候应该这么调用:

1
2
3
4
5
6
7
8
9
10
query {
grade(gradeId: 1) {
id
name
classes(gradeId: 1) {
id
name
}
}
}

因为 classes 需要传参数,那我们也需要在请求的时候带上参数,可是我们能不能不传参数呢?因为如果把数据整体看作一个 JSON 的话我们只要拿到 grade 就应该能拿到他的年级,而不是在传一次参数,而且如果参数不一致那就会出现查错的问题。不要急,当然是可以的。我们看到 resolvers 里面类型的实现中第一个参数 root 一直没用到,他的作用就是为了这种级联查询。我们先打印一下它,看他是啥

1
2
3
4
5
6
7
8
9
Grade: {
classes: (root, { gradeId }) => {
console.log(root);
const classes = database.class.filter(
classItem => classItem.gradeId === gradeId
);
return classes;
}
}

打印出来后是不是发现啥了,它就是它父级的数据,那是不是可以从父级数据中拿到 gradeId,来我们改造一下,

1
2
3
4
5
type Grade {
id: Int
name: String
classes: [Class]
}

把 classes 的参数去掉

1
2
3
4
5
6
7
8
9
Grade: {
classes: root => {
const gradeId = root.id;
const classes = database.class.filter(
classItem => classItem.gradeId === gradeId
);
return classes;
}
}

把 resolvers 的类型实现改改,从 root 中获取 gradeId
我们在调用一次

1
2
3
4
5
6
7
8
9
10
query {
grade(gradeId: 1) {
id
name
classes {
id
name
}
}
}

是不是依然可以,我们现在把其他类型的关联查询也加上。我们可以进行多级的查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
query {
grade(gradeId: 1) {
id
name
classes {
id
name
students {
id
name
}
}
}
}

现在回过头来对比着看看我们写的 typeDefs 和 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
module.exports.typeDefs = gql`
type Query {
grade(gradeId: Int!): Grade
class(classId: Int!): Class
student(studentId: Int!): Student
}

type Grade {
id: Int
name: String
classes: [Class]
}

type Class {
id: Int
name: String
gradeId: Int
students: [Student]
}

type Student {
id: Int
name: String
classId: Int
}
`;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module.exports.resolvers = {
Query: {
grade: (root, { gradeId }) => { ... },
class: (root, { classId }) => { ... },
student: (root, { studentId }) => { ... }
},

Grade: {


classes: root => { ... }
},

Class: {



students: root => { ... }
}
};
这样两边一对比是不是很多清楚了,resolvers是对typeDefs里面各个类型的字段的实现,不仅仅是自定义类型的字段可以用resolvers实现,Int等基础类型的也可以重新实现一遍, 比如:
1
2
3
4
Grade: {
name: root => root.name + "(北京校区)",
classes: root => { ... }
},

现在是不是感觉到 graphql 的魅力了?接下来的东西才是它真正魅力所在
我们为 grade 类型的 classes 字段的 resolver 加一个日志

1
2
3
4
5
6
7
8
9
10
11
Grade: {
name: root => root.name + '(北京校区)',
classes: root => {
console.log('查询了班级数据表!');
const gradeId = root.id;
const classes = database.class.filter(
classItem => classItem.gradeId === gradeId
);
return classes;
}
},

然后接着请求数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
query {
grade(gradeId: 1) {
id
name
classes {
id
name
students {
id
name
}
}
}
}

看到日志打印出来了,那我们如果不查询 classes 试试

1
2
3
4
5
6
query {
grade(gradeId: 1) {
id
name
}
}

是不是没有打日志,这就是 graphql,你要啥我给啥,你不要我就不去查

如果理解了上面这些,那 mutation 就很简单了,它和 query 基本一样,只不过它可能会改动数据库,并且你关注的可能是结果,但是你把结果看成是查询的字段拿它是不是就很容易就理解了

好了,今天就到这,改天我们接着讲前端如何运用,其实猜一猜的话它就一个请求地址,而且他的 GET 请求被可视化界面占用,那他应该是所有接口用同一个 POST 请求,那么怎么区分各个请求呢?其实很简单一句话:graphql 是一套标准。想到了吗?

课件地址

https://github.com/yangjiagongzi/graphql-study

下一节:graphql 前端页面的应用