上一篇我们介绍了 Apollo Federation 的架构以及一些开发原则,本篇我们来结合代码说明如何借助于 Federation 来拆分单体 BFF 服务。
应用架构
在前面的 BFF 单体服务中,我们定义了两个模型,商品模型和订单模型。本文中,我们划分两个领域,商品域和订单域。这两个域分别由一个服务来承载,加上网关,我们的应用架构如下:
开发拆分后的子服务
商品子服务
商品服务的所有代码在这里open in new window,文中只展示关键点。
定义商品服务:
import { Directive, Field, ObjectType } from '@nestjs/graphql';
@ObjectType({ description: '商品信息' })
@Directive('@key(fields: "goodsId")')
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
14
通过 nestjs 的 Directive
装饰器,我们使用 Apollo Federation 的 @key
指令告诉 Federation 可以使用 goodsId
来查询 GoodsData
实例信息。
同时我们在 GoodsResolver
中添加一个 ResolveReference
,来实现在 Federation 场景下的查找动作。
@Resolver(() => GoodsData)
export class GoodsResolver {
constructor(private readonly goodsService: GoodsService) {}
// 其他业务代码
@ResolveReference()
resolveReference(reference: {
__typename: string;
goodsId: string;
}): Promise<GoodsData> {
return this.goodsService.getGoods({ goodsId: reference.goodsId });
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
除了上面的改造以外,我们还需将原来 src/app.module.ts
中的 GraphQLModule
换成 GraphQLFederationModule
,其他保持不变。
@Module({
imports: [
GraphQLFederationModule.forRoot({
introspection: true, // 生产环境中需要关闭
path: '/goods/graphql',
autoSchemaFile: true,
// 其他配置信息
} as GqlModuleOptions),
],
controllers: [AppController],
providers: [...services, ...resolvers],
})
export class AppModule {}
2
3
4
5
6
7
8
9
10
11
12
13
至此,新的商品子服务就改造完成了。
启动服务。
yarn start:dev
访问 http://localhost:3001/goods/graphql
。
我们可以像使用单体服务一样使用新的商品子服务。
订单子服务
订单子服务的代码在这里open in new window,文中只展示关键点。
因为订单要引用商品模型,拆分以后,我们需要在订单中定义一下对商品模型的引用。
import { Directive, Field, ObjectType } from '@nestjs/graphql';
@ObjectType({ description: '商品信息' })
@Directive('@extends')
@Directive('@key(fields: "goodsId")')
export class GoodsData {
@Field(() => String, { nullable: false, description: '商品 ID' })
@Directive('@external')
goodsId: string;
}
2
3
4
5
6
7
8
9
10
通过 @extends
指令告诉 Federation GoodsData
继承自一个其他服务定义的类型。@key
指令告诉 Federation 使用 goodsId
作为主键,同时在 goodsId
字段上的@external
指令告诉 Federation 这个字段也是来自于其他服务定义的。
在订单服务中,我们可以在这个
GoodsData
上定义订单服务额外增加的字段,此处不做过多说明。关于类型如何定义,Federation 指令如何使用,可以参考 Federation 文档open in new window.
定义好 GoodsData
以后,我们需要在订单模型中定义引用这个类型。
@ObjectType({ description: '订单数据' })
export class OrderData {
// 其他字段定义
@Field(() => GoodsData, { nullable: true, description: '商品信息' })
@Directive('@provides(fields: "goodsId")')
goodsData?: GoodsData;
}
2
3
4
5
6
7
8
@provides
指令告诉 Federation 订单服务可以提供商品信息的查询。
同时,我们需要对 OrderResolver
做一些改造。
@Resolver(() => OrderData)
export class OrderPropertyResolver {
@ResolveField(() => GoodsData, {
name: 'goodsData',
nullable: true,
})
goodsData(@Parent() orderData: OrderData): any {
return { __typename: 'GoodsData', goodsId: orderData.goodsId };
}
}
2
3
4
5
6
7
8
9
10
在 goodsData
方法中,我们不再直接查商品数据库,而是返回一个 Reference 对象,包含两个字段,__typename
告诉 Federation 引用的具体的类型名称,goodsId
就是我们在商品子服务中通过 @key
定义的主键。
我们同样需要对 src/app.module.ts
做一些改造。
@Module({
imports: [
HttpModule,
GraphQLFederationModule.forRoot({
// 其他配置项
introspection: true, // 生产环境中需要关闭
path: '/order/graphql',
autoSchemaFile: true,
buildSchemaOptions: {
orphanedTypes: [GoodsData],
},
} as GqlModuleOptions),
],
controllers: [AppController],
providers: [...services, ...resolvers],
})
export class AppModule {}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
我们将 GraphQLModule
换成 GraphQLFederationModule
,同时通过 buildSchemaOptions
将 GoodsData
标记为其他系统定义的类型。
开发网关(Gateway)
子服务只定义了领域内的类型,以及类型间的依赖关系,Apollo Federation 的网关在运行时将会分析查询信息,根据类型间的关系将请求分发到各个子服务上去。
网关的完整代码在这里open in new window,本文只针对关键代码做说明。
Federation 网关的代码非常简单,如下:
import { Module } from '@nestjs/common';
import { GraphQLGatewayModule } from '@nestjs/graphql';
const graphqlGatewayModule = GraphQLGatewayModule.forRootAsync({
useFactory: async () => ({
server: {
path: '/bff/graphql',
},
gateway: {
serviceList: [
{
name: 'goods',
url: 'http://localhost:3001/goods/graphql',
},
{
name: 'order',
url: 'http://localhost:3002/order/graphql',
},
],
},
}),
});
@Module({
imports: [graphqlGatewayModule],
})
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
27
我们通过 serviceList
指定了两个子服务。网关在启动后,会根据 serviceList
拉取子服务的 schema。
上图中我们可以看出,从网关查询数据与前面单体服务完全相同,客户端完全感知不到差别。
总结
本文借助于 NestJS,对如何使用 Apollo Federation 拆分 BFF 单体服务做了样例说明。
我们定义了商品和订单两个子服务,订单服务中引用了商品服务的模型。
我们还定义了一个网关来聚合子服务,并对外提供服务。在网关中我们通过 serviceList
来定义服务列表。从工程实践的角度来说,这种方式非常不便,每新增一个服务都需要更改这个配置,下一篇我们来介绍如何解决这个问题。
关注微信公众号,获取最新推送~
加微信,深入交流~