本文通过搭建一个简单的商品订单系统,来介绍如何基于 GraphQL、NestJS 构建一个 BFF 服务。
TypeScript 这几年迅猛发展,不仅在前端开发中使用越来越广泛,在 NodeJS 端也逐步铺开。本文选择基于 TypeScript 开发的服务端框架 NestJSopen in new window 和已经被社区广泛使用的 Apolloopen in new window 来实现 BFF 服务。
项目初始化
我们直接使用 NestJS 的 CLI(命令行)工具来初始化项目,假设我们的应用名称是 bff-graphql-server
。
yarn global add @nestjs/cli
nest new bff-graphql-server
2
注意,在写这篇文章的时候,NestJS 官方刚刚将 NestJS 的版本升级到 v8,一些周边的依赖包存在版本不匹配问题,因此建议使用 v7 版本,或者直接 fork demo 项目 repoopen in new window。
初始化完成后,我们添加 GraphQL 依赖。
yarn add @nestjs/graphql graphql-tools graphql apollo-server-express
至此,我们已经完成了项目初始化。
添加应用代码
在开始写业务代码之前,我们先规划一下目录结构。
默认初始化出来的项目目录如下:
-- src
-- main.ts // 系统启动入口
-- app.module.ts // 应用主模块
-- app.controller.ts // 控制器
-- app.service.ts // 服务,用于获取数据
2
3
4
5
我们新增加三个目录,分别是 models
、services
、resolvers
:
-- src
-- modules // 存放模型定义,最终生成 GraphQL 的 Schema
-- resolvers // 存放 Resolver,实现数据的聚合裁减
-- services // 对接后端服务接口,用于获取数据
2
3
4
在 BFF 中任何一个领域模型,都需要一份模型定义,一个获取数据的 Service
和一个用于数据聚合和裁减的 Resolver
。
在这个例子中,我们需要定义两个模型,商品模型和订单模型。
初始化 GraphQLModule
在 NestJS 中,我们通过 module 来管理代码,因此,我们需要初始化 GraphQLModule
。
修改默认的 src/app.module.ts
代码如下:
import { HttpModule, Module } from '@nestjs/common';
import { GqlModuleOptions, GraphQLModule } from '@nestjs/graphql';
import { AppController } from './app.controller';
import resolvers from './resolvers';
import services from './services';
@Module({
imports: [
HttpModule,
GraphQLModule.forRoot({
debug: true, // 生产环境中需要关闭
introspection: true, // 开启内省模式,生产环境中需要关闭
path: '/graphql', // 定义 GraphQL 的请求路径
autoSchemaFile: true, // 自动生成 schema 文件
playground: { // UI 界面
// 生产环境中需要关闭
settings: {
'request.credentials': 'same-origin',
},
},
} as GqlModuleOptions),
],
controllers: [AppController],
providers: [...services, ...resolvers], // 系统中定义的 service 和 resolver
})
export class AppModule {}
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
商品模型
定义模型
在 models
目录下新建一个 goods
目录:
-- src
-- models
-- goods
2
3
定义商品模型,在这个模型中,商品有三个字段:
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType({ description: '商品信息' })
export class GoodsData {
@Field(() => String, { nullable: false, description: '商品 ID' })
goodsId: string;
@Field(() => String, { nullable: false, description: '商品名称' })
goodsName: string;
@Field(() => String, { nullable: true, description: '商品简介' })
goodsBrief?: string;
}
2
3
4
5
6
7
8
9
10
11
12
13
现在我们定义查询商品模型的参数:
import { Field, InputType } from '@nestjs/graphql';
@InputType({ description: '商品详情参数' })
export class GoodsParam {
@Field(() => String, { nullable: false, description: '商品 ID' })
goodsId: string;
}
2
3
4
5
6
7
定义 Service
在 services
目录下新建 goods
目录:
-- src
-- services
-- goods
2
3
创建一个 GoodsService
类:
import { HttpService, Injectable } from '@nestjs/common';
import { GoodsData, GoodsParam } from 'src/models/goods/model';
@Injectable()
export class GoodsService {
constructor(private readonly http: HttpService) {}
getGoods(param: GoodsParam): Promise<GoodsData> {
return this.http
.get<GoodsData>('/goodsservice/rest/goods/detail', { params: param })
.toPromise()
.then((resp) => resp.data);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
我们假定通过
/goodsservice/rest/goods/detail
可以查询商品详情。HttpService
是 NestJS 提供的基于 Axiosopen in new window 的封装。
定义好 GoodsService
以后,我们将这个类添加到 src/services/index.ts
中导出。
import { GoodsService } from './goods';
export default [GoodsService];
2
3
定义 Resolver
在 resolvers
目录下新建 goods
目录:
-- src
-- resolvers
-- goods
2
3
创建一个 GoodsResolver
类:
import { Args, Query, Resolver } from '@nestjs/graphql';
import { GoodsData, GoodsParam } from 'src/models/goods/model';
import { GoodsService } from 'src/services/goods/goods';
@Resolver()
export class GoodsResolver {
constructor(private readonly goodsService: GoodsService) {}
@Query(() => GoodsData, {
name: 'goodsData',
})
goods(@Args('param') param: GoodsParam): Promise<GoodsData> {
return this.goodsService.getGoods(param.goodsId);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
定义好 GoodsResolver
以后,我们将这个类添加到 src/resolvers/index.ts
中导出。
import { GoodsResolver } from './goods';
export default [GoodsResolver];
2
3
完成以后,启动进程,我们就可以在 playground 里面看到定义的商品模型了,如下图。
我们在 playground 中做一些查询看看效果。
订单模型
与商品模型类似,我们需要做一些定义。
定义模型
在 models
目录下新建一个 orders
目录:
-- src
-- models
-- orders
2
3
定义订单模型和查询参数,如下:
import { Field, Float, InputType, ObjectType } from '@nestjs/graphql';
@ObjectType({ description: '订单数据' })
export class OrderData {
@Field(() => String, { nullable: false, description: '订单号' })
orderNo: string;
@Field(() => String, { nullable: false, description: '商品 ID' })
goodsId: string;
@Field(() => Float, { nullable: false, description: '金额' })
pay: number;
@Field(() => String, { nullable: true, description: '备注信息' })
comment?: string;
}
@InputType({ description: '订单详情参数' })
export class OrderParam {
@Field(() => String, { nullable: false, description: '订单号' })
orderNo: string;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
需要注意的是,OrderData
中有一个 goodsId
字段与商品建立关系,我们后面在 Resolver
中需要借助于这个字段来查询商品信息。
定义 Service
在 services
目录下新建 orders
目录:
-- src
-- services
-- orders
2
3
创建一个 OrderService
类:
import { HttpService, Injectable } from '@nestjs/common';
import { OrderData, OrderParam } from 'src/models/orders/model';
@Injectable()
export class OrderService {
constructor(private readonly http: HttpService) {}
getOrder(param: OrderParam): Promise<OrderData> {
return this.http
.get<OrderData>('/goodsservice/rest/order/detail', { params: param })
.toPromise()
.then((resp) => resp.data);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
别忘了在
src/services/index.ts
添加OrderService
的导出。
定义 Resolver
在 resolvers
目录下新建 orders
目录:
-- src
-- resolvers
-- orders
2
3
创建一个 OrderResolver
类:
import { Args, Query, Resolver } from '@nestjs/graphql';
import { OrderData OrderParam } from 'src/models/orders/model';
import { OrderService } from 'src/services/orders/order';
@Resolver()
export class OrderResolver {
constructor(private readonly orderService: OrderService) {}
@Query(() => OrderData, {
name: 'orderData',
})
order(@Args('param') param: OrderParam): Promise<OrderData> {
return this.orderService.getOrder(param);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
注意,别忘了在
src/resolvers/index.ts
中添加OrderResolver
的导出。
定义好 OrderResolver
以后,我们就可以查询订单信息。
这时候,如果我们直接查询订单上的商品信息是查不到,因为还没建立相关的 Resolver。
我们现在添加一个 OrderPropertyResolver
类:
import { Args, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
import { GoodsData } from 'src/models/goods/model';
import { OrderData } from 'src/models/orders/model';
import { GoodsService } from 'src/services/goods/goods';
@Resolver(() => OrderData)
export class OrderPropertyResolver {
constructor(private readonly goodsService: GoodsService) {}
@ResolveField(() => GoodsData, {
name: 'goodsData',
nullable: true,
})
goodsData(@Parent() orderData: OrderData): Promise<GoodsData> {
return this.goodsService.getGoodsById(orderData.goodsId);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
注意这里 @Resolver(() => OrderData)
的写法,它告诉 GraphQL,当需要解析 OrderData
上的字段信息时,使用这个 Resolver 类。
我们通过 @Parent()
装饰器可以拿到 OrderData
上的数据,在这个例子中就是 goodsId
。
这里要额外说一点,通常来说,单个订单直接通过商品详情接口查商品数据没有问题,但是如果是订单列表要查商品数据,依然通过商品详情接口查的话就会产生 N + 1 问题了。
在前面的文章中我们提到过,可以借助于 DataLoaderopen in new window 来解决 N + 1 问题。
我们对 GoodsService
做一些改造,引入 DataLoader。
import DataLoader from 'dataloader';
@Injectable()
export class GoodsService {
// 其他代码
private goodsLoader = new DataLoader<string, GoodsData>(
async (goodsIdList: readonly string[]) => {
const goodsList = await this.batchGetGoodsByGoodsId(
goodsIdList as string[],
);
return goodsIdList.map((goodsId) =>
goodsList.find((goodsData) => goodsData.goodsId === goodsId),
);
},
);
private batchGetGoodsByGoodsId(goodsIdList: string[]): Promise<GoodsData[]> {
return this.http
.get<GoodsData[]>('/goodsservice/rest/goods/list-by-id', {
params: goodsIdList,
})
.toPromise()
.then((resp) => {
return resp.data;
});
}
getGoodsById(goodsId: string): Promise<GoodsData> {
return this.goodsLoader.load(goodsId);
}
getGoodsByIdList(goodsIdList: string[]): Promise<(GoodsData | Error)[]> {
return this.goodsLoader.loadMany(goodsIdList);
}
}
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
正如代码中所示,DataLoader 依赖后端提供一个批量接口,即通过
goodsId
批量查询GoodsData
信息。
完成以后,我们就可以愉快的在查询订单的时候也查询商品信息了。
小结
我们通过搭建一个简单的订单、商品系统展示了如果开发一个基于 GraphQL 的 BFF 服务。
结合 NestJS 和 Apollo,我们可以方便的通过 TypeScript 来开发 GraphQL 服务,极大的提升开发效率。
借助于 GraphQL 的 playground,我们可以方便的对系统进行查询,不依赖前端代码实现。
关注微信公众号,获取最新推送~
加微信,深入交流~