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
25const 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将项目启动在 Playground 中左半边输入
1
2
3query {
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 的官方文档,发现内容和细节都很多,那接下来你可以跟着我做,做完之后呢,你就知道文档中的内容你应该怎么运用了。
首先我们要知道query和mutation,文档上叫他们两为“查询”和“变更”,意思就是说一个管读,一个管写,你所有的 api 都应该在这里登记一下,这样 graphql 才知道你要干啥。query和mutation 你可以理解为是两颗树的主干,你可以给他添一些枝干。那一个接口是应该放在query还是放在mutation,是怎么区分的呢?其实是靠人为区分的,他没有一个规定来保证你放的位置是正确的,比如,刚刚的 hello 接口,我们把它放在 mutation 中,他仍然可以返回。
我们把 main.js 改成这样子:
1 | const Koa = require('koa'); |
我们请求
1 | mutation { |
依然可以返回结果,这个约定其实类似 GET 请求和 POST 请求的约定,靠大家自觉遵守。
接下来就是改造项目了,首先我们应该将typeDefs和resolvers分离开来,不然项目后期难以维护,那我们在项目中新建一个 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
17const 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
7const { gql } = require('apollo-server-koa');
module.exports.typeDefs = gql`
type Query {
hello: String
}
`;resolvers.js
1
2
3
4
5module.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
26const { 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
30const 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 | query { |
但是呢如果我要查询一个年级下有几个班级,这个就查不到了,那我们怎么做呢?我们期望的是在年级的类型中增加一个数组来表示改年级下的班级,就像是这样:
1 | type Grade { |
那我们就把 Grade 的类型先改成这样,我们执行查询
1 | query { |
看查询的结果 classes 是 null,到这里同学们应该想到了我们应该修改 resolvers.js。是的,没错,resolvers.js 就是对 typeDefs 的实现。那么我们该怎么修改呢?可能有的同学会说,直接修改 resolvers 中的 grade 的实现,让他把 class 查出来一起返回。这样确实也能达到目的,但是却不是 graphql 想要的效果。让我们仔细观察一下 typeDefs,看到它里面所有的定义都是类型定义,我们在回想 Query 是树干,其他类型是枝干的伏笔,那我们是不是可以发现其实 “type Grade” 和 “type Query” 长得很像?
如果是下面这样子那是不是就看起来一摸一样了
1 | type Query { |
那我们应该就知道 resolvers 里面该怎样写了:给 resolvers 增加一个类型
1 | const database = require('../database'); |
那我们调用的时候应该这么调用:
1 | query { |
因为 classes 需要传参数,那我们也需要在请求的时候带上参数,可是我们能不能不传参数呢?因为如果把数据整体看作一个 JSON 的话我们只要拿到 grade 就应该能拿到他的年级,而不是在传一次参数,而且如果参数不一致那就会出现查错的问题。不要急,当然是可以的。我们看到 resolvers 里面类型的实现中第一个参数 root 一直没用到,他的作用就是为了这种级联查询。我们先打印一下它,看他是啥
1 | Grade: { |
打印出来后是不是发现啥了,它就是它父级的数据,那是不是可以从父级数据中拿到 gradeId,来我们改造一下,
1 | type Grade { |
把 classes 的参数去掉
1 | Grade: { |
把 resolvers 的类型实现改改,从 root 中获取 gradeId
我们在调用一次
1 | query { |
是不是依然可以,我们现在把其他类型的关联查询也加上。我们可以进行多级的查询
1 | query { |
现在回过头来对比着看看我们写的 typeDefs 和 resolvers
|
|
1 | Grade: { |
现在是不是感觉到 graphql 的魅力了?接下来的东西才是它真正魅力所在
我们为 grade 类型的 classes 字段的 resolver 加一个日志
1 | Grade: { |
然后接着请求数据
1 | query { |
看到日志打印出来了,那我们如果不查询 classes 试试
1 | query { |
是不是没有打日志,这就是 graphql,你要啥我给啥,你不要我就不去查
如果理解了上面这些,那 mutation 就很简单了,它和 query 基本一样,只不过它可能会改动数据库,并且你关注的可能是结果,但是你把结果看成是查询的字段拿它是不是就很容易就理解了
好了,今天就到这,改天我们接着讲前端如何运用,其实猜一猜的话它就一个请求地址,而且他的 GET 请求被可视化界面占用,那他应该是所有接口用同一个 POST 请求,那么怎么区分各个请求呢?其实很简单一句话:graphql 是一套标准。想到了吗?
课件地址
https://github.com/yangjiagongzi/graphql-study