nest-graphql

nest-graphql

七月 31, 2019

市面上介绍Graphql的文章不知凡几,好坏都有。那为什么今天还要介绍Nest当中的Grahpql的使用方法呢?

  • 无需学习成本
    Graphql实际上有自己的一套语言规范,例如schema规范等等,所以需要一定量的额外学习成本,这也是国内Graphql落地较少的原因之一。 而Nest解决了这个问题,其schema定义包括查询语法等基本和其自身的接口查询完美融合,只要会开发Nest接口,自然而然的也就会开发Graphql接口。
  • 天然适配BFF层
    现在很多公司开始使用微服务架构,数据也不再是简单的left join。会根据业务拆分服务,数据存在不同的数据库中。 而前端页面展示的数据也可能是从不同的服务(接口或者各类DB)中组装而成。而Graphql只需定义完Schema后,按需每个field自己去取就可以了。(但是这里的数据拆分包括DB拆分其实需要高端解耦人才,性能也要考虑,不过这个交给后端去考虑吧)

我们先来看下graphql的界面接口调试图:
image.png
界面的左半部分就是查询语法,而右面就是我们定义的Schema,包括了查询格式,返回值格式,字段类型等等。

现在我们开始,首先明确需求,我们有两个数据表,一张是作者表,一张是文章表。我们要实现几个接口:

  • 作者列表
  • 根据作者id获取作者
  • 修改文章数据
  • 订阅修改文章数据的操作

前期准备:

  • 安装包

    1
    npm i --save @nestjs/graphql apollo-server-express graphql
  • 准备数据

    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
    // 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开始。

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
// 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自行学习。

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
// 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,实际的业务操作

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
// 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层大同小异,写法基本也相同

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
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Module({
imports: [
AuthorsModule,
GraphQLModule.forRoot({
debug: false,
playground: true, // 开启调试界面
autoSchemaFile: './schema.gql', // 放个该名字的空文件,底层会读取Nest形式的schema然后生成graphql原始的sehema里面
installSubscriptionHandlers: true, // 使用订阅就要开启这个参数
})
],
controllers: [AppController],
providers: [AppService],
})

大功告成!