市面上介绍Graphql的文章不知凡几,好坏都有。那为什么今天还要介绍Nest当中的Grahpql的使用方法呢?
- 无需学习成本
Graphql实际上有自己的一套语言规范,例如schema规范等等,所以需要一定量的额外学习成本,这也是国内Graphql落地较少的原因之一。 而Nest解决了这个问题,其schema定义包括查询语法等基本和其自身的接口查询完美融合,只要会开发Nest接口,自然而然的也就会开发Graphql接口。 - 天然适配BFF层
现在很多公司开始使用微服务架构,数据也不再是简单的left join。会根据业务拆分服务,数据存在不同的数据库中。 而前端页面展示的数据也可能是从不同的服务(接口或者各类DB)中组装而成。而Graphql只需定义完Schema后,按需每个field自己去取就可以了。(但是这里的数据拆分包括DB拆分其实需要高端解耦人才,性能也要考虑,不过这个交给后端去考虑吧)
我们先来看下graphql的界面接口调试图:
image.png
界面的左半部分就是查询语法,而右面就是我们定义的Schema,包括了查询格式,返回值格式,字段类型等等。
现在我们开始,首先明确需求,我们有两个数据表,一张是作者表,一张是文章表。我们要实现几个接口:
- 作者列表
- 根据作者id获取作者
- 修改文章数据
- 订阅修改文章数据的操作
前期准备:
- 安装包
npm i --save @nestjs/graphql apollo-server-express graphql
- 准备数据
// author.ts
import { Author } from '../graphqlSchema/author';
const aurthors: Author[] = [
{
id: 1,
firstName: '孙',
lastName: '悟空',
posts: [],
},
{
id: 2,
firstName: '猪',
lastName: '八戒',
posts: [],
},
{
id: 3,
firstName: '沙',
lastName: '悟净',
posts: [],
},
{
id: 4,
firstName: '唐',
lastName: '僧',
posts: [],
},
{
id: 5,
firstName: '白',
lastName: '龙马',
posts: [],
},
{
id: 6,
firstName: '哪吒',
lastName: '三台子',
posts: [],
},
{
id: 7,
firstName: '李',
lastName: '靖',
posts: [],
},
{
id: 8,
firstName: '太乙',
lastName: '真人',
posts: [],
},
{
id: 9,
firstName: '元始',
lastName: '天尊',
posts: [],
},
{
id: 10,
firstName: '申',
lastName: '公豹',
posts: [],
},
{
id: 11,
firstName: '龙',
lastName: '王',
posts: [],
},
];
export default aurthors;
// posts.ts
import { Post } from '../graphqlSchema/post';
const posts: Post[] = [
{
id: 1,
title: '三打白骨精',
votes: 4,
},
{
id: 1,
title: '真假大圣',
votes: 4,
},
{
id: 6,
title: '大闹龙宫',
votes: 5,
},
];
export default posts;
那第一步,我们先从定义Schema开始。
// author.ts
import { Field, Int, ObjectType } from 'type-graphql';
import { Post } from './post';
@ObjectType()
export class Author {
@Field(type => Int)
id: number; // 作者id
@Field({ nullable: true })
firstName?: string; // 姓
@Field({ nullable: true })
lastName?: string; // 名
@Field(type => [Post])
posts: Post[]; // 文章列表
}
// post.ts
import { Field, Int, ObjectType } from 'type-graphql';
@ObjectType()
export class Post {
@Field(type => Int)
id: number; // 作者id
@Field()
title: string; // 文章标题
@Field(type => Int, { nullable: true })
votes?: number; // 投票数
}
我们前面说过Nest中的Graqhql无需学习额外的语法。所以Schema也和Nest普通定义dto差不多。 只不过上面@ObjectType是用来标示这是Graphql的Schema,表示实体,返回的数据格式。 nullable代表是否可为空。
第二步我们来定义接口的入参,既然是入参,我们当然还要判断入参的类型等等是否符合规范,这里面统一用了class-validator来判断。 不会的请去github自行学习。
// author.args.ts
import { Min, IsNotEmpty } from 'class-validator';
import { ArgsType, Field, Int } from 'type-graphql';
@ArgsType()
export class AuthorArgs {
@Field(type => Int)
@IsNotEmpty()
@Min(0)
// pageIndex: number = 0;
pageIndex;
}
// post.args.ts
import { InputType, Field } from 'type-graphql';
import { IsNotEmpty } from 'class-validator';
@InputType()
export class UpvotePostInput {
@Field()
@IsNotEmpty()
id: number;
@Field()
@IsNotEmpty()
votes: number;
}
我们可以看到入参的schema其实和Nest的dto也仍是一致。 @ArgsType这时就代表了该类实际是graphql的查询接口的参数格式。 @InputType则是修改或者插入接口的参数格式。
下来就是编写service,实际的业务操作
// author.service.ts
import { Injectable } from '@nestjs/common';
import { Author } from '../graphqlSchema/author';
import * as authors from '../models/author';
import * as _ from 'lodash';
import { AuthorArgs } from '../dto/author.args';
@Injectable()
export class AuthorsService {
// 查询作者列表,简单的从已经预先创建好的数据进行分块,每块10条数据
authorList(authorArgs: AuthorArgs): Author[] {
const chunk = _.chunk(authors.default, 10);
return chunk[authorArgs.pageIndex];
}
// 根据id查询作者
findOne(id: number): Author {
return _.find(authors.default, (item) => item.id === id);
}
}
// posts.service.ts
import { Injectable } from '@nestjs/common';
import { Post } from '../graphqlSchema/post';
import * as posts from '../models/posts';
import * as _ from 'lodash';
import { UpvotePostInput } from '../dto/post.args';
@Injectable()
export class PostsService {
// 根据作者id查询所有文章
findAll(id): Post[] {
return _.filter(posts.default, (item) => item.id === id);
}
// 根据id修改该文章的投票数
upvoteById(upvotePostInput: UpvotePostInput): Post {
const post = _.find(posts.default, (item) => item.id === upvotePostInput.id);
post.votes = upvotePostInput.votes;
return post;
}
}
service部分大功告成,下来就是编写resolver层了,这里和Nest的Controller层大同小异,写法基本也相同
import { Args, Mutation, Query, Resolver, Subscription, ResolveProperty, Parent } from '@nestjs/graphql';
import { PubSub } from 'apollo-server-express';
import { Author } from '../graphqlSchema/author';
import { AuthorsService } from '../service/author.service';
import { PostsService } from '../service/posts.service';
import { AuthorArgs } from '../dto/author.args';
import { Post } from '../graphqlSchema/post';
import { UpvotePostInput } from '../dto/post.args';
const pubSub = new PubSub();
@Resolver(of => Author)
export class AuthorResolver {
constructor(
private readonly authorsService: AuthorsService,
private readonly postsService: PostsService,
) { }
// 第一个查询节点,名字叫做authorList,可以对比最上面的graphql界面图
@Query(returns => [Author])
// 作者列表需要分页,所以穿个pageIndex,我们已经在上面定义过了
authorList(@Args() authorArgs: AuthorArgs): Author[] {
return this.authorsService.authorList(authorArgs);
}
// 修改文章的投票数,graphql中修改操作是叫Mutation
@Mutation(returns => Post)
// 修改的节点叫做upvotePost
upvotePost(@Args('upvotePostData') upvotePostInput: UpvotePostInput) {
const post = this.postsService.upvoteById(upvotePostInput);
// graphql中有个操作叫做订阅,即前端如果订阅了某个事件(这里是postupdated),则当有此事件发布时,
// 前端会收到通知,实际上是通过websocket实现的。 这里即当投票数修改时,会发送此事件。
pubSub.publish('postupdated', { postupdated: post });
return post;
}
// 第二个查询节点。即根据id查询作者
@Query(returns => Author)
// 如果参数不多,不必定义类似dto的schema,直接写也可
author(@Args('id') id: number): Author {
return this.authorsService.findOne(id);
}
// 这里就是posts这个filed单独查询数据
@ResolveProperty()
// 根据父元素的id去查询
posts(@Parent() author) {
const { id } = author;
return this.postsService.findAll(id);
}
// 前端的订阅接口
@Subscription(returns => Post)
postupdated() {
return pubSub.asyncIterator('postupdated');
}
}
Nest最大特色就是依赖注入,所以我们还要写module。
// author.graphql.module.ts
import { Module } from '@nestjs/common';
import { PostsModule } from './posts.graphql.module';
import { AuthorsService } from '../service/author.service';
import { AuthorResolver } from '../resolver/author.resolver';
@Module({
imports: [PostsModule], // 因为AuthorResolver中用到了postService,所以要导入该模块
providers: [AuthorsService, AuthorResolver],
})
export class AuthorsModule { }
// posts.graphql.module.ts
import { Module } from '@nestjs/common';
import { PostsService } from '../service/posts.service';
@Module({
providers: [PostsService],
exports: [PostsService], // 因为有别的模块需要使用,所以要导出该模块
})
export class PostsModule { }
最后在app.module.ts这个根模块中,我们要开启graphql模块。
@Module({
imports: [
AuthorsModule,
GraphQLModule.forRoot({
debug: false,
playground: true, // 开启调试界面
autoSchemaFile: './schema.gql', // 放个该名字的空文件,底层会读取Nest形式的schema然后生成graphql原始的sehema里面
installSubscriptionHandlers: true, // 使用订阅就要开启这个参数
})
],
controllers: [AppController],
providers: [AppService],
})
大功告成!