
GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。
大概率你听说过 GraphQL,知道它是一种与 Rest API 架构属于 API 接口的查询语言。但大概率你也与我一样没有尝试过 GraphQL。
事实上从 2012 年 Facebook 首次将 GraphQL 应用于移动应用,到 GraphQL 规范于 2015 年实现开源。可如今现状是 GraphQL 不温不火,时不时又有新的文章介绍,不知道的还以为是什么新技术。
本文将上手使用 GraphQL,并用 Nestjs 与 Strapi 这两个 Node 框架搭建 GraphQL 服务。
关于 GraphQL 介绍,详见官网 GraphQL | A query language for your API 或相关介绍视频 GraphQL 速览:React/Vue 的最佳搭档
GraphQL 与 Restful API 相比

Restful API
Restful 架构的设计范式侧重于分配 HTTP 请求方法(GET、POST、PUT、PA TCH、DELETE)和 URL 端点之间的关系。如下图

但是实际复杂的业务中,单靠 Restful 接口,需要发送多条请求,例如获取博客中某篇博文数据与作者数据
GET /blog/1
GET /blog/1/author
要么单独另写一个接口,如getBlogAndAuthor,这样直接为调用方“定制”一个接口,请求一条就得到就调用方想要的数据。但是另写一个getBlogAndAuthor 就破坏了 Restful API 接口风格,并且在复杂的业务中,比如说还要获取博文的评论等等,后端就要额外提供一个接口,可以说非常繁琐了。
有没有这样一个功能,将这些接口做一下聚合,然后将结果的集合返回给前端呢?在目前比较流行微服务架构体系下,有一个专门的中间层专门来处理这个事情,这个中间层叫 BFF(Backend For Frontend)。可以参阅 BFF——服务于前端的后端

但这些接口一般来说都比较重,里面有很多当前页面并不需要的字段,那还有没有一种请求:客户端只需要发送一次请求就能获取所需要的字段
有,也就是接下来要说的 GraphQL
GraphQL

REST API 构建在请求方法(method)和端点(endpoint)之间的连接上,而 GraphQL API 被设计为只通过一个端点,即 /graphql,始终使用 POST 请求进行查询,其集中的 API 如 http://localhost:3000/graphql,所有的操作都通过这个接口来执行,这会在后面的操作中在展示到。
而服务端要做的就是搭建一个 GraphQL 服务,这在后面也会操作到,也算是本文的重点。 :::
接下来便会在客户端中体验下 GraphQL,看看 GraphQL 究竟有多好用。
在线体验 GraphQL
可以到 官网 中简单尝试入门一下,在 Studio 可在线体验 GraphQL,也可以到 SWAPI GraphQL API 中体验。
下面以 apollographql 为例,并查询 People 对象。
query
查询所有 People 并且只获取 name、gender、height 字段

查询 personID 为 1 的 Person 并且只获取 name,gender,height 字段

查询 personID 为 2 的 Person 并且只获取 name,eyeColor、skinColor、hairColor 字段

从上面查询案例中其实就可以发现,我只需要在 person 中写上想要获取的字段,GraphQL 便会返回带有该字段的数据。避免了返回结果中不必要的数据字段。
{
person{
# 写上想获取的字段
}
}
如果你不想要 person 数据或者想要其他其他的数据,不用像 Restful API 那样请求多条接口,依旧请求/graphql,如

无论你想要什么数据,一次请求便可满足。
mutation
GraphQL 的大部分讨论集中在数据获取(也是它的强项),但是任何完整的数据平台也都需要一个改变服务端数据的方法。即 CRUD。
GraphQL 提供了 变更(Mutations) 用于改变服务端数据,不过 apollographql 在线示例中并没有如 createPeople 字段支持 。这个片段在线体验中就无法体验到,后在后文中展示到。这里你只需要知道 GraphQL 能够执行基本的 CRUD 即可。
fragmen 和 subscribtion
此外还有 fragment 与 subscription 就不做介绍。
小结
尝试完上面这些操作后,可以非常明显的感受到 GraphQL 的优势与便利,本来是需要请求不同的 url,现在只需要请求 /graphql,对调用方(前端)来说非常友好,香是真的香。
可目前只是使用了别人配置好的 GraphQL 服务,让前端开发用了特别友好的 API。但是,对于后端开发而言,想要提供 GraphQL 服务可就不那么友善了。因为它不像传统的 restful 请求,需要专门配置 GraphQL 服务,而整个过程是需要花费一定的工作量(定义 Schema,Mutations 等等),前面也提到想要一条请求就能得到客户端想要的数据字段,那服务端必然需要额外的工作量。
不仅需要在后端中配置 GraphQL 服务,用于接收 GraphQL 查询并验证和执行,此外前端通常需要 GraphQL 客户端,来方便使用 GraphQL 获取数据,目前实用比较多的是Apollo Graph,不过本文侧重搭建 GraphQL 服务,因此前端暂不演示如何使用 GraphQL。
你可能听过一句话是,graphql 大部分时间在折磨后端,并且要求比较严格的数据字段,但是好处都是前端。把工作量基本都丢给了后端,所以在遇到使用这门技术的公司,尤其是后端岗位就需要考虑有没有加班的可能了。
以下便会开始实际搭建 GraphQL 服务,这里会用 Nest.js 与 Strapi 分别实践演示。
Nest.js
官方文档:GraphQL + TypeScript | NestJS
仓库本文实例代码仓库: kuizuo/nest-graphql-demo
创建项目
nest new nest-graphql-demo
安装依赖
npm i @nestjs/graphql @nestjs/apollo graphql apollo-server-express
修改 app.module.ts
import { Module } from '@nestjs/common'
import { GraphQLModule } from '@nestjs/graphql'
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: true,
}),
],
})
export class AppModule {}
resolver
设置了autoSchemaFile: true ,nest.js 将会自动搜索整个项目所有以 .resolver.ts 为后缀的文件,将其解析为 schema.gql 比如说创建app.resolver.ts
import { Resolver, Query } from '@nestjs/graphql'
@Resolver()
export class AppResolver {
@Query(() => String) // 定义一个查询,并且返回字符类型
hello() {
return 'hello world'
}
}
在 graphql 中 resolver 叫解析器,与 service 类似(也需要在 @Module 中通过 providers 导入)。resolver主要包括query(查询数据)、mutation(增、删、改数据)、subscription(订阅,有点类型 socket),在 graphql 项目中我们用 resolver 替换了之前的控制 器。
这时候打开http://127.0.0.1:3000/graphql,可以在右侧中看到自动生成的 Schema,这个 Schema 非常关键,决定了你客户端能够请求到什么数据。
尝试输入 GraphQL 的 query 查询(可以按 Ctrl + i 触发代码建议(Trigger Suggest),与 vscode 同理)

此时点击执行,可以得到右侧结果,即app.resolver.ts 中 hello 函数所定义的返回体。

Code first 与 Schema first
在 nestjs 中有 Code first 与 Schema first 两种方式来生成上面的 Schema,从名字上来看,前者是优先定义代码会自动生成 Schema,而后者是传统方式先定义 Schema。
在上面一开始的例子中是 Code First 方式,通常使用该方式即可,无需关心 Schema 是如何生成的。下文也会以 Code First 方式来编写 GraphQL 服务。
也可到官方示例仓库中 nest/sample/31-graphql-federation-code-first 和 nest/sample/32-graphql-federation-schema-first 查看两者代码上的区别。
快速生成 GraphQL 模块
nest 提供 cli 的方式来快速生成 GraphQL 模块
nest g resource <name>

比如创建一个 blog 模块
nest g resource blog --no-spec
? What transport layer do you use? GraphQL (code first)
? Would you like to generate CRUD entry points? Yes
CREATE src/blog/blog.module.ts (217 bytes)
CREATE src/blog/blog.resolver.ts (1098 bytes)
CREATE src/blog/blog.resolver.spec.ts (515 bytes)
CREATE src/blog/blog.service.ts (623 bytes)
CREATE src/blog/blog.service.spec.ts (446 bytes)
CREATE src/blog/dto/create-blog.input.ts (196 bytes)
CREATE src/blog/dto/update-blog.input.ts (243 bytes)
CREATE src/blog/entities/blog.entity.ts (187 bytes)
UPDATE src/app.module.ts (643 bytes)
便会生成如下文件

import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql'
import { BlogService } from './blog.service'
import { Blog } from './entities/blog.entity'
import { CreateBlogInput } from './dto/create-blog.input'
import { UpdateBlogInput } from './dto/update-blog.input'
@Resolver(() => Blog)
export class BlogResolver {
constructor(private readonly blogService: BlogService) {}
@Mutation(() => Blog)
createBlog(@Args('createBlogInput') createBlogInput: CreateBlogInput) {
return this.blogService.create(createBlogInput)
}
@Query(() => [Blog], { name: 'blogs' })
findAll() {
return this.blogService.findAll()
}
@Query(() => Blog, { name: 'blog' })
findOne(@Args('id', { type: () => Int }) id: number) {
return this.blogService.findOne(id)
}
@Mutation(() => Blog)
updateBlog(@Args('updateBlogInput') updateBlogInput: UpdateBlogInput) {
return this.blogService.update(updateBlogInput.id, updateBlogInput)
}
@Mutation(() => Blog)
removeBlog(@Args('id', { type: () => Int }) id: number) {
return this.blogService.remove(id)
}
}
此时 Schema 如下

不过nest cli创建的blog.service.ts 只是示例代码,并没有实际业务的代码。
此外blog.entity.ts也不为数据库实体类,因此这里引入typeorm,并使用sqlite3
集成 Typeorm
安装依赖
pnpm install @nestjs/typeorm typeorm sqlite3
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { GraphQLModule } from '@nestjs/graphql'
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'
import { AppResolver } from './app.resolver'
import { BlogModule } from './blog/blog.module'
import { TypeOrmModule } from '@nestjs/typeorm'
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'db.sqlite3',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
}),
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: true,
playground: true,
}),
AppModule,
BlogModule,
],
controllers: [AppController],
providers: [AppService, AppResolver],
})
export class AppModule {}
将 blog.entity.ts 改成实体类,代码为
import { ObjectType, Field } from '@nestjs/graphql'
import {
Column,
Entity,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm'
@ObjectType()
@Entity()
export class Blog {
@Field(() => Int)
@PrimaryGeneratedColumn()
id: number
@Field()
@Column()
title: string
@Field()
@Column({ type: 'text' })
content: string
@Field()
@CreateDateColumn({ name: 'created_at', comment: '创建时间' })
createdAt: Date
@Field()
@UpdateDateColumn({ name: 'updated_at', comment: '更新时间' })
updatedAt: Date
}
其中 @ObjectType() 装饰器让 @nestjs/graphql 自动让其视为一个 type Blog
而 @Field() 则是作为可展示的字段,比如 password 字段无需返回,就不必要加该装饰器。
@Field() 是件繁琐的事情(nest 官方自然也想到),于是提供了 GraphQL + TypeScript - CLI Plugin 用于省略 @Field() 等其他操作。(类似于语法糖)借用官方的话:
Thus, you won't have to struggle with @Field decorators scattered throughout the code.
因此,您不必为分散在代码中的@Field 装饰符而烦恼。 :::
@nestjs/graphql 会将 typescript 的 number 类型视为 Float,所以需要转成 Int 类型,即 @Field(() => Int)
为 BlogService 编写 CRUD 数据库业务代码,并在 dto 编写参数效验代码,这里简单暂时部分代码。
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { CreateBlogInput } from './dto/create-blog.input'
import { UpdateBlogInput } from './dto/update-blog.input'
import { Blog } from './entities/blog.entity'
@Injectable()
export class BlogService {
constructor(
@InjectRepository(Blog)
private blogRepository: Repository<Blog>,
) {}
create(createBlogInput: CreateBlogInput) {
return this.blogRepository.save(createBlogInput)
}
findAll() {
return this.blogRepository.find()
}
findOne(id: number) {
return this.blogRepository.findOneBy({ id })
}
async update(id: number, updateBlogInput: UpdateBlogInput) {
const blog = await this.blogRepository.findOneBy({ id })
const item = { ...blog, ...updateBlogInput }
return this.blogRepository.save(item)
}
remove(id: number) {
return this.blogRepository.delete(id)
}
}
import { InputType, Field } from '@nestjs/graphql'
@InputType()
export class CreateBlogInput {
@Field()
title: string
@Field()
content: string
}
此时

CRUD
下面将演示 graphql 的 Mutation。
新增

