夜猫子的知识栈 夜猫子的知识栈
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《Web Api》
    • 《ES6教程》
    • 《Vue》
    • 《React》
    • 《TypeScript》
    • 《Git》
    • 《Uniapp》
    • 小程序笔记
    • 《Electron》
    • JS设计模式总结
  • 《前端架构》

    • 《微前端》
    • 《权限控制》
    • monorepo
  • 全栈项目

    • 任务管理日历
    • 无代码平台
    • 图书管理系统
  • HTML
  • CSS
  • Nodejs
  • Midway
  • Nest
  • MySql
  • 其他
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • Ajax
  • Vite
  • Vitest
  • Nuxt
  • UI库文章
  • Docker
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

夜猫子

前端练习生
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《Web Api》
    • 《ES6教程》
    • 《Vue》
    • 《React》
    • 《TypeScript》
    • 《Git》
    • 《Uniapp》
    • 小程序笔记
    • 《Electron》
    • JS设计模式总结
  • 《前端架构》

    • 《微前端》
    • 《权限控制》
    • monorepo
  • 全栈项目

    • 任务管理日历
    • 无代码平台
    • 图书管理系统
  • HTML
  • CSS
  • Nodejs
  • Midway
  • Nest
  • MySql
  • 其他
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • Ajax
  • Vite
  • Vitest
  • Nuxt
  • UI库文章
  • Docker
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Node基础

  • 《MySQL》学习笔记

  • Midway

  • Nest

    • 开篇词
    • 学习理由
    • nest概念扫盲
    • 快速掌握 nestcli
    • 5种http数据传输方式
    • IoC 解决了什么痛点问题?
    • 如何调试 Nest 项目
    • Provider注入对象
    • 全局模块和生命周期
    • AOP 架构有什么好处?
    • 一网打尽 Nest 全部装饰器
    • Nest如何自定义装饰器
    • Metadata和Reflector
    • ExecutionContext切换上下文
    • Module和Provider的循环依赖处理
    • 如何创建动态模块
    • Nest和Express,fastify
    • Nest的Middleware
    • RxJS和Interceptor
    • 内置Pipe和自定义Pipe
    • ValidationPipe验证post请求参数
    • 如何自定义 Exception Filter
    • 图解串一串 Nest 核心概念
    • 接口如何实现多版本共存
    • Express如何使用multer实现文件上传
    • Nest使用multer实现文件上传
    • 图书管理系统
    • 大文件分片上传
    • 最完美的 OSS 上传方案
    • Nest里如何打印日志
    • 为什么Node里要用Winston打印日志
    • Nest 集成日志框架 Winston
    • 通过Desktop学Docker也太简单了
    • 你的第一个 Dockerfile
    • Nest 项目如何编写 Dockerfile
    • 提升 Dockerfile 水平的 5 个技巧
    • Docker 是怎么实现的
    • 为什么 Node 应用要用 PM2 来跑?
    • 快速入门 MySQL
    • SQL 查询语句的所有语法和函数
    • 一对一、join 查询、级联方式
    • 一对多、多对多关系的表设计
    • 子查询和 EXISTS
    • SQL 综合练习
    • MySQL 的事务和隔离级别
    • MySQL 的视图、存储过程和函数
    • Node 操作 MySQL 的两种方式
    • 快速掌握 TypeORM
    • TypeORM 一对一的映射和关联 CRUD
    • TypeORM 一对多的映射和关联 CRUD
    • TypeORM 多对多的映射和关联 CRUD
    • 在 Nest 里集成 TypeORM
    • TypeORM保存任意层级的关系
    • 生产环境为什么用TypeORM的migration迁移功能
    • Nest 项目里如何使用 TypeORM 迁移
    • 如何动态读取不同环境的配置?
    • 快速入门 Redis
    • 在 Nest 里操作 Redis
    • 为什么不用 cache-manager 操作 Redis
    • 两种登录状态保存方式:JWT、Session
    • Nest 里实现 Session 和 JWT
    • MySQL + TypeORM + JWT 实现登录注册
    • 基于 ACL 实现权限控制
    • 基于 RBAC 实现权限控制
    • access_token和refresh_token实现无感登录
    • 单token无限续期实现登录无感刷新
    • 使用 passport 做身份认证
    • passport 实现 GitHub 三方账号登录
    • passport 实现 Google 三方账号登录
    • 为什么要使用 Docker Compose ?
    • Docker 容器通信的最简单方式:桥接网络
    • Docker 支持重启策略,是否还需要 PM2
    • 快速掌握 Nginx 的 2 大核心用法
    • 基于 Nginx 实现灰度系统
    • 基于 Redis 实现分布式 session
    • Redis + 高德地图,实现附近的充电宝
    • 用 Swagger 自动生成 api 文档
    • 如何灵活创建 DTO
    • class- validator 的内置装饰器,如何自定义装饰器
    • 序列化 Entity,你不需要 VO 对象
    • 手写序列化 Entity 的拦截器
    • 使用 compodoc 生成文档
    • Node 如何发邮件?
    • 实现基于邮箱验证码的登录
    • 基于 sharp 实现 gif 压缩工具
    • 大文件如何实现流式下载?
    • Puppeteer 实现爬虫,爬取 BOSS 直聘全部前端岗位
    • 实现扫二维码登录
    • Nest 的 REPL 模式
    • 实现 Excel 导入导出
    • 如何用代码动态生成 PPT
    • 如何拿到服务器 CPU、内存、磁盘状态
    • Nest 如何实现国际化?
    • 会议室预订系统:需求分析和原型图
    • 会议室预订系统:技术方案和数据库设计
    • 会议室预订系统:用户管理模块--用户注册
    • 会议室预订系统:用户管理模块--配置抽离、登录认证鉴权
    • 会议室预订系统:用户管理模块-- interceptor、修改信息接口
    • 会议室预订系统:用户管理模块--用户列表和分页查询
    • 会议室预订系统:用户管理模块-- swagger 接口文档
    • 会议室预订系统:用户管理模块-- 用户端登录注册页面
    • 会议室预订系统:用户管理模块-- 用户端信息修改页面
    • 会议室预订系统:用户管理模块-- 头像上传
    • 会议室预订系统:用户管理模块-- 管理端用户列表页面
    • 会议室预订系统:用户管理模块-- 管理端信息修改页面
    • 会议室预订系统:会议室管理模块-后端开发
    • 会议室预订系统:会议室管理模块-管理端前端开发
    • 会议室预订系统:会议室管理模块-用户端前端开发
    • 会议室预订系统:预定管理模块-后端开发
    • 会议室预订系统:预定管理模块-管理端前端开发
    • 会议室预订系统:预定管理模块-用户端前端开发
    • 会议室预订系统:统计管理模块-后端开发
    • 会议室预订系统:统计管理模块-前端开发
    • 会议室预订系统:后端项目部署到阿里云
    • 会议室预订系统:前端项目部署到阿里云
    • 会议室预定系统:用 migration 初始化表和数据
    • 会议室预定系统:文件上传 OSS
    • 会议室预定系统:Google 账号登录后端开发
    • 会议室预定系统:Google 账号登录前端开发
    • 会议室预定系统:后端代码优化
    • 会议室预定系统:集成日志框架 winston
    • 会议室预定系统:前端代码优化
    • 会议室预定系统:全部功能测试
    • 会议室预定系统:项目总结
    • Nest 如何创建微服务?
    • Nest 的 Monorepo 和 Library
    • 用 Etcd 实现微服务配置中心和注册中心
    • Nest 集成 Etcd 做注册中心、配置中心
    • 用 Nacos 实现微服务配置中心和注册中心
    • 基于 gRPC 实现跨语言的微服务通信
    • 快速入门 ORM 框架 Prisma
    • Prisma 的全部命令
    • Prisma 的全部 schema 语法
    • Primsa Client 单表 CRUD 的全部 api
    • Prisma Client 多表 CRUD 的全部 api
    • 在 Nest 里集成 Prisma
    • 为什么前端监控系统要用 RabbitMQ?
    • 基于 Redis 实现关注关系
    • 基于 Redis 实现各种排行榜(周榜、月榜、年榜)
    • 考试系统:需求分析
    • 考试系统:技术方案和数据库设计
    • 考试系统:微服务、Lib 拆分
    • 考试系统;用户注册
    • 考试系统:用户登录、修改密码
    • 考试系统:考试微服务
    • 考试系统:登录、注册页面
    • 考试系统:修改密码、试卷列表页面
    • 考试系统:新增试卷、回收站
    • 考试系统:试卷编辑器
    • 考试系统:试卷回显、预览、保存
    • 考试系统:答卷微服务
    • 考试系统:答题页面
    • 考试系统:自动判卷
    • 考试系统:分析微服务、排行榜页面
    • 考试系统:整体测试
    • 考试系统:项目总结
    • 用 Node.js 手写 WebSocket 协议
    • Nest 开发 WebSocket 服务
    • 基于 Socket.io 的 room 实现群聊
    • 聊天室:需求分析和原型图
    • 聊天室:技术选型和数据库设计
    • 聊天室:用户注册
    • 聊天室:用户登录
    • 聊天室:修改密码、修改信息
    • 聊天室:好友列表、发送好友申请
    • 聊天室:创建聊天室、加入群聊
    • 聊天室:登录、注册页面开发
    • 聊天室:修改密码、信息页面开发
    • 聊天室:头像上传
    • 聊天室:好友∕群聊列表页面
    • 聊天室:添加好友弹窗、通知页面
    • 聊天室:聊天功能后端开发
    • 聊天室:聊天功能前端开发
    • 聊天室:一对一聊天
    • 聊天室:创建群聊、进入群聊
    • 聊天室:发送表情、图片、文件
    • 聊天室:收藏
    • 聊天室:全部功能测试
    • 聊天室:项目总结
    • MongoDB 快速入门
    • 使用 mongoose 操作 MongoDB 数据库
    • GraphQL 快速入门
      • 总结
    • Nest 开发 GraphQL 服务:实现 CRUD
    • GraphQL + Primsa + React 实现 TodoList
    • 如何调试 Nest 源码?
  • 其他

  • 服务端
  • Nest
神说要有光
2025-03-10
目录

GraphQL 快速入门

作为前端开发,想必经常做的事情就是:调接口、画页面、调接口、画页面...

调用的接口大概率是 restful 的,也就是类似这种:

/students 查询所有学生信息

/student/1 查询 id 为 1 的学生信息

上面说的是 get 请求。

如果对 /student/1 发送 POST、PUT、DELETE 请求,就分别代表了新增、修改、删除。

这就是 restful 风格的 web 接口。

这种接口返回什么信息是服务端那边决定的,客户端只是传一下参数。

而不同场景下需要的数据不同,这时候可能就得新开发一个接口。特别是在版本更新的时候,接口会有所变动。

这样就很容易导致一大堆类似的接口。

facebook 当时也遇到了这个问题,于是他们创造了一种新的接口实现方案:GraphQL。

用了 GraphQL 之后,返回什么数据不再是服务端说了算,而是客户端自己决定。

服务端只需要提供一个接口,客户端通过这个接口就可以取任意格式的数据,实现 CRUD。

比如想查询所有的学生,就可以这样:

想再查询他们的年龄,就可以这样:

想查询老师的名字和他教的学生,就可以这样:

而这些都是在一个 http 接口里完成的!

感受了 GraphQL 的好处了没?

一个 http 接口就能实现所有的 CRUD!

那这么强大的 GraphQL 是怎么实现的呢?

我们先写个 demo 快速入门一下:

facebook 提供了 graphql 的 npm 包,但那个封装的不够好,一般我们会用基于 graphql 包的 @apollo/server 和 @apollo/client 的包来实现 graphql。

mkdir graphql-crud-demo
cd graphql-crud-demo
npm init -y
1
2
3

安装用到的包:

npm install @apollo/server
1

然后在 index.js 写一下这段代码:

import { ApolloServer } from '@apollo/server';

const typeDefs = `
  type Student {
    id: String,
    name: String,
    sex: Boolean
    age: Int
  }

  type Teacher {
    id: String,
    name: String,
    age: Int,
    subject: [String],
    students: [Student]
  }

  type Query {
    students: [Student],
    teachers: [Teacher],
  }

  schema {
    query: Query
  }
`;
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

比较容易看懂,定义了一个 Student 的对象类型,有 id、name、sex、age 这几个字段。

又定义了一个 Teacher 的对象类型,有 id、name、age、subject、students 这几个字段。students 字段是他教的学生的信息。

然后定义了查询的入口,可以查 students 和 teachers 的信息。

这样就是一个 schema。

对象类型和对象类型之间有关联关系,老师关联了学生、学生也可以关联老师,关联来关联去这不就是一个图么,也就是 graph。

GraphQL 全称是 graph query language,就是从这个对象的 graph 中查询数据的。

现在我们声明的只是对象类型的关系,还要知道这些类型的具体数据,取数据的这部分叫做 resolver。

const students = [
    {
      id: '1',
      name: async () => {
        await '取数据';
        return '光光'
      },
      sex: true,
      age: 12
    },
    {
      id: '2',
      name:'东东',
      sex: true,
      age: 13
    },
    {
      id: '3',
      name:'小红',
      sex: false,
      age: 11
    },
];

const teachers = [
  {
    id: '1',
    name: '神光',
    sex: true,
    subject: ['体育', '数学'],
    age: 28,
    students: students
  }
]

const resolvers = {
    Query: {
      students: () => students,
      teachers: () => teachers
    }
};
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
39
40
41

resolver 是取对象类型对应的数据的,每个字段都可以写一个 async 函数,里面执行 sql、访问接口等都可以,最终返回取到的数据。

当然,直接写具体的数据也是可以的。

这样有了 schema 类型定义,有了取数据的 resovler,就可以跑起 graphql 服务了。

也就是这样:

import { startStandaloneServer } from '@apollo/server/standalone' 

const server = new ApolloServer({
    typeDefs,
    resolvers,
});
  
const { url } = await startStandaloneServer(server, {
    listen: { port: 4000 },
});
  
console.log(`🚀  Server ready at: ${url}`);
1
2
3
4
5
6
7
8
9
10
11
12

传入 schema 类型定义和取数据的 resolver,就可以用 node 把服务跑起来。

有同学可能问了,node 可以直接解析 esm 模块么?

可以的。只需要在 package.json 中声明 type 为 module:

那所有的 .js 就都会作为 esm 模块解析:

跑起来之后,浏览器访问一下:

就可以看到这样的 sandbox,这里可以执行 graphql 的查询:

(graphql 接口是监听 POST 请求的,用 get 请求这个 url 才会跑这个调试的工具)

我查询所有学生的 id、name、age 就可以这样:

query Query {
    students {
        name,
        id
    }
}
1
2
3
4
5
6

这里 “光光” 那个学生是异步取的数据,resolver 会执行对应的异步函数,拿到最终数据:

取老师的信息就可以这样:

这样我们就实现了一个 graphql 接口!

感觉到什么叫客户端决定取什么数据了么?

当然,我们这里是在 sandbox 里测的,用 @apollo/client 包也很简单。

比如 react 的 graphql 客户端是这样的:

一个 gql 的 api 来写查询语言,一个 useQuery 的 api 来执行查询。

学起来很简单。

我们之后还是直接在 sandbox 里测试。

有的同学可能会说,如果我想查询某个名字的老师的信息呢?

怎么传参数?

graphql 当然是支持的,这样写:

type Query {
    students: [Student],
    teachers: [Teacher],
    studentsbyTeacherName(name: String!): [Student]
}
1
2
3
4
5

新加一个 query 入口,声明一个 name 的参数。(这里 String 后的 ! 代表不能为空)

然后它对应的 resolver 就是这样的:

const resolvers = {
    Query: {
      students: () => students,
      teachers: () => teachers,
      studentsbyTeacherName: async (...args) => {
        console.log(args);

        await '执行了一个异步查询'
        return students
      }
    }
};
1
2
3
4
5
6
7
8
9
10
11
12

studentsbyTeacherName 字段的 resolver 是一个异步函数,里面执行了查询,然后返回了查到的学生信息。

我们打印下参数看看传过来的是什么。

有参数的查询是这样的:

传入老师的 name 参数为 111,返回查到的学生的 id、name 信息。

可以看到返回的就是查询到的结果。

而服务端的 resolver 接收到的参数是这样的:

其余的几个参数不用管,只要知道第二个参数就是客户端传过来的查询参数就好了。

这样我们就可以根据这个 name 参数实现异步的查询,然后返回数据。

这就实现了有参数的查询。

不是说 graphql 能取代 restful 做 CRUD 么?那增删改怎么做呢?

其实看到上面的有参数的查询应该就能想到了,其实写起来差不多。

在 schema 里添加这样一段类型定义:

type Res {
    success: Boolean
    id: String
}

type Mutation {
    addStudent(name:String! age:Int! sex:Boolean!): Res

    updateStudent(id: String! name:String! age:Int! sex:Boolean!): Res

    deleteStudent(id: String!): Res
}

schema {
    mutation: Mutation
    query: Query
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

和有参数的查询差不多,只不过这部分增删改的类型要定义在 mutation 部分。

然后 resolver 也要有对应的实现:

async function addStudent (_, { name, age, sex }) {
    students.push({
        id: '一个随机 id',
        name,
        age,
        sex
    });
    return {
      success: true,
      id: 'xxx'
    }
}

async function updateStudent (_, { id, name, age, sex }) {

    return {
      success: true,
      id: 'xxx'
    }
}

async function deleteStudent (_, { id }) {
    return {
      success: true,
      id: 'xxx'
    }
}
  
const resolvers = {
    Query: {
      students: () => students,
      teachers: () => teachers,
      studentsbyTeacherName: async (...args) => {
        console.log(args);

        await '执行了一个异步查询'
        return students
      }
    },
    Mutation: {
        addStudent: addStudent,
        updateStudent: updateStudent,
        deleteStudent: deleteStudent
    }
};

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
39
40
41
42
43
44
45
46

和 query 部分差不多,只不过这里实现的是增删改。

我只对 addStudent 做了实现。

我们测试下:

执行 addStudent,添加一个学生:

然后再次查询所有的学生:

就可以查到刚来的小刚同学。

这样,我们就可以在一个 graphql 的 POST 接口里完成所有的 CRUD!

全部代码如下,大家可以跑一跑(注意要在 package.json 里加个 type: "module")

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone' 

const typeDefs = `
  type Student {
    id: String,
    name: String,
    sex: Boolean
    age: Int
  }

  type Teacher {
    id: String,
    name: String,
    age: Int,
    subject: [String],
    students: [Student]
  }

  type Query {
    students: [Student],
    teachers: [Teacher],
    studentsbyTeacherName(name: String!): [Student]
  }

  type Res {
    success: Boolean
    id: String
  }

  type Mutation {
    addStudent(name:String! age:Int! sex:Boolean!): Res

    updateStudent(id: String! name:String! age:Int! sex:Boolean!): Res

    deleteStudent(id: String!): Res
  }

  schema {
    mutation: Mutation
    query: Query
  }
`;

const students = [
    {
      id: '1',
      name: async () => {
        await '取数据';
        return '光光'
      },
      sex: true,
      age: 12
    },
    {
      id: '2',
      name:'东东',
      sex: true,
      age: 13
    },
    {
      id: '3',
      name:'小红',
      sex: false,
      age: 11
    },
];

const teachers = [
  {
    id: '1',
    name: '神光',
    sex: true,
    subject: ['体育', '数学'],
    age: 28,
    students: students
  }
]

async function addStudent (_, { name, age, sex }) {
    students.push({
        id: '一个随机 id',
        name,
        age,
        sex
    });
    return {
      success: true,
      id: 'xxx'
    }
}

async function updateStudent (_, { id, name, age, sex }) {

    return {
      success: true,
      id: 'xxx'
    }
}

async function deleteStudent (_, { id }) {
    return {
      success: true,
      id: 'xxx'
    }
}
  
const resolvers = {
    Query: {
      students: () => students,
      teachers: () => teachers,
      studentsbyTeacherName: async (...args) => {
        console.log(args);

        await '执行了一个异步查询'
        return students
      }
    },
    Mutation: {
        addStudent: addStudent,
        updateStudent: updateStudent,
        deleteStudent: deleteStudent
    }
};

const server = new ApolloServer({
    typeDefs,
    resolvers,
});
  
const { url } = await startStandaloneServer(server, {
    listen: { port: 4000 },
});
  
console.log(`🚀  Server ready at: ${url}`);
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135

完成了 graphql 的入门,我们再稍微思考下它的原理。graphql 是怎么实现的呢?

回顾整个流程,我们发现涉及到两种 DSL(领域特定语言),一个是 schema 定义的 DSL,一个是查询的 DSL。

服务端通过 schema 定义的 DSL 来声明 graph 图,通过 resolver 来接受参数,执行查询和增删改。

客户端通过查询的 DSL 来定义如何查询和如何增删改,再发给服务端来解析执行。

通过这种 DSL 实现了动态的查询。

确实很方便很灵活,但也有缺点,就是 parse DSL 为 AST 性能肯定是不如 restful 那种直接执行增删改查高的。

具体要不要用 graphql 还是要根据具体场景来做判断。

案例代码在小册仓库 (opens new window)。

# 总结

restful 接口是 url 代表资源,GET、POST、PUT、DELETE 请求代表对资源的增删改查。

这种接口返回什么数据完全由服务端决定,每次接口变动可能就得新加一种接口。

为了解决这种问题,facebook 创造了 graphql,这种接口返回什么数据完全由客户端决定。增删改查通过这一个接口就可以搞定。

graphql 需要在服务端定义 schema,也就是定义对象类型和它的字段,对象类型和对象类型之间会有关联,也就是一个 graph,查询就是从这个 graph 里查询数据。

除了 schema 外,还需要有 resolver,它负责接受客户端的参数,完成具体数据的增删改查。

graphql 会暴露一个 post 接口,通过查询语言的语法就可以从通过这个接口完成所有增删改查。

本地测试的时候,get 请求会跑一个 sandbox,可以在这里测试接口。

整个流程涉及到两种新语言: schema 定义语言和 query 查询语言。入门之后向深入的话就是要学下这两种 DSL 的更多语法。

感受到 graphql 的强大之处了么?一个接口就可以实现所有的 CRUD!

编辑 (opens new window)
上次更新: 2025/10/27 10:53:52
使用 mongoose 操作 MongoDB 数据库
Nest 开发 GraphQL 服务:实现 CRUD

← 使用 mongoose 操作 MongoDB 数据库 Nest 开发 GraphQL 服务:实现 CRUD→

最近更新
01
H5调用微信jssdk
09-28
02
VueVirtualScroller
09-19
03
如何调试 Nest 项目
03-10
更多文章>
Copyright © 2019-2025 Study | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式