上一篇我们介绍了 Apollo Federation 的架构以及一些开发原则,本篇我们来结合代码说明如何借助于 Federation 来拆分单体 BFF 服务。

应用架构

在前面的 BFF 单体服务中,我们定义了两个模型,商品模型和订单模型。本文中,我们划分两个领域,商品域和订单域。这两个域分别由一个服务来承载,加上网关,我们的应用架构如下:

uml diagram

开发拆分后的子服务

商品子服务

商品服务的所有代码在这里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;
}
1
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 });
  }
}
1
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 {}
1
2
3
4
5
6
7
8
9
10
11
12
13

至此,新的商品子服务就改造完成了。

启动服务。

yarn start:dev
1

访问 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;
}
1
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;
}
1
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 };
  }
}
1
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 {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

我们将 GraphQLModule 换成 GraphQLFederationModule,同时通过 buildSchemaOptionsGoodsData 标记为其他系统定义的类型。

开发网关(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 {}
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

我们通过 serviceList 指定了两个子服务。网关在启动后,会根据 serviceList 拉取子服务的 schema。

从网关查数据

上图中我们可以看出,从网关查询数据与前面单体服务完全相同,客户端完全感知不到差别。

总结

本文借助于 NestJS,对如何使用 Apollo Federation 拆分 BFF 单体服务做了样例说明。

我们定义了商品和订单两个子服务,订单服务中引用了商品服务的模型。

我们还定义了一个网关来聚合子服务,并对外提供服务。在网关中我们通过 serviceList 来定义服务列表。从工程实践的角度来说,这种方式非常不便,每新增一个服务都需要更改这个配置,下一篇我们来介绍如何解决这个问题。

关注微信公众号,获取最新推送~