nestjs微服务-系列3

112 阅读3分钟

封装独立的 GRPC 模块

通过之前的两篇文章,我们已经搭建起来 nestjs 的微服务项目。并使用 consul 作为注册中心,并封装了 consul 模块,提供了服务注册的方法和通过服务名获取服务的方法。

此篇文章,我将要创建一个 GrpcModule,它应该具备以下能力:

  1. 服务端能力: 能够轻松地将一个 NestJS 应用注册为 gRPC 服务。
  2. 客户端能力: 提供一个动态的 registerClient 方法。任何其他模块或服务只需要提供服务名称(例如 'user-service'),GrpcModule 就能自动通过 ConsulService 解析其地址和端口,并创建一个可以被注入和使用的 gRPC 客户端。

首先,初始化 grpc 模块。

nest g lib grpc

目录结构为:

/
├── libs
│   ├── consul
│   │   ├── src
│   │   │   ├── consul.module.ts
│   │   │   └── consul.service.ts  <-- 已存在的模块
│   ├── grpc
│   │   ├── src
│   │   │   ├── grpc.module.ts       <-- 要创建的核心模块
│   │   │   ├── grpc.providers.ts    <-- 动态客户端的提供者工厂
│   │   │   ├── interfaces           <-- (可选)定义配置接口
│   │   │   │   └── grpc-client-options.interface.ts
│   │   │   └── index.ts             <-- 方便地导出所有内容
│   └── ...
├── apps
│   ├── gateway                <-- gRPC 客户端示例
│   ├── user-service           <-- gRPC 服务端示例
│   └── ...
└── ...

第 一 步:定义配置接口

创建一个接口来规范 registerClient 方法的参数。

libs/grpc/src/interfaces/grpc-client-options.interface.ts

import { Transport } from '@nestjs/microservices';

export interface GrpcClientOptions {
  // 用于注入客户端的 token
  injectToken: string | symbol;
  // 在 Consul 中注册的服务名称
  serviceName: string;
  // proto 文件的包名
  packageName: string;
  // proto 文件相对于项目根目录的路径
  protoPath: string;
  // (可选)自定义 Transport 类型,默认为 gRPC
  transport?: Transport.GRPC;
}

第 二 步:创建动态客户端提供者工厂 (grpc.providers.ts)

这是整个方案的核心。创建一个函数,它会生成一个 NestJS 的工厂提供者(Factory Provider)。这个提供者会:

  1. 注入您已经封装好的 ConsulService
  2. 使用 ConsulServiceresolveAddress 方法来获取服务的 addressport
  3. 使用 NestJS 内置的 ClientProxyFactory 来创建一个 gRPC 客户端实例。

libs/grpc/src/grpc.providers.ts

import { Logger, Provider } from '@nestjs/common';
import { GrpcClientOptions } from '@app/grpc/interfaces/grpc-client-options.interface';
import { ConsulService } from '@libs/consul';
import { ClientProxyFactory, Transport } from '@nestjs/microservices';
import { join } from 'path';
import * as process from 'node:process';

/**
 * 创建 gRPC 客户端提供者的工厂函数
 * @param options - 客户端配置
 * @returns Provider
 */
export function createGrpcClientProvider(options: GrpcClientOptions): Provider {
  const logger = new Logger('createGrpcClientProvider');
  const { injectToken, serviceName, packageName, protoPath } = options;
  return {
    // 提供者的注入令牌
    provide: injectToken,
    useFactory: async (consulService: ConsulService) => {
      logger.log(`[gRPC Client] Resolving address for service: ${serviceName}`);

      // 1. 动态解析服务地址
      const serviceAddress = await consulService.resolveAddress(serviceName);
      if (!serviceAddress) {
        logger.error(
          `[gRPC Client] Could not resolve address for service: ${serviceName}. Is it running and registered in Consul?`,
        );
      }
      const { address, port } = serviceAddress;
      const url = `${address}:${port}`;
      logger.log(`[gRPC Client] Service '${serviceName}' found at ${url}`);

      // 2. 使用 ClientProxyFactory 创建 gRPC 客户端
      return ClientProxyFactory.create({
        transport: Transport.GRPC,
        options: {
          package: packageName,
          protoPath: join(process.cwd(), 'proto', protoPath),
          url,
          // 可以在这里添加其他 gRPC 选项,例如凭证、超时等
          // loader: { keepCase: true },
        },
      });
    },
    // 3. 注入 ConsulService 以便在工厂函数中使用它
    inject: [ConsulService],
  };
}

第三步:创建 GrpcModule (grpc.module.ts)

这个模块将是核心入口。我们将使用 NestJS 的动态模块(Dynamic Module)功能,因为它允许我们传递配置并动态地注册提供者(Provider)。

libs/grpc/src/grpc.module.ts

import { DynamicModule, Module, Provider } from '@nestjs/common';
import { GrpcClientOptions } from './interfaces/grpc-client-options.interface';
import { ConsulModule } from '@libs/consul';
import { createGrpcClientProvider } from './grpc.provides';

@Module({})
export class GrpcModule {
  /**
   * 注册一个或多个 gRPC 客户端
   * @param options - 客户端配置数组
   * @returns DynamicModule
   */
  static registerClient(options: GrpcClientOptions | GrpcClientOptions[]): DynamicModule {
    const clientOptions = Array.isArray(options) ? options : [options];
    // 根据配置创建所有的 gRPC 客户端提供者
    const clientProviders = clientOptions.flatMap(
      (option): Provider => createGrpcClientProvider(option),
    );

    return {
      module: GrpcModule,
      global: true,
      imports: [ConsulModule],
      // 注册动态创建的提供者
      providers: [...clientProviders],
      // 导出这些提供者,以便其他模块可以注入它们
      exports: [...clientProviders],
    };
  }
}

第四步:导出模块 (index.ts)

为了方便地从库中导入,创建一个 index.ts 文件。

libs/grpc/src/index.ts

export * from './grpc.module';
export * from './interfaces/grpc-client-options.interface';

gateway 中注册

此时,我们将在 app.module.ts中, 只需要调用 GrpcModule.registerClient方法就能实现服务发现

import { ConsulModule } from '@libs/consul';
import { Module } from '@nestjs/common';
import { UserModule } from './user/user.module';
import { GrpcModule } from '@app/grpc';
import {
  USER_PACKAGE_NAME,
  USER_SERVICE_NAME,
} from '../../../proto/generated/user.interface';
import {
  ORDER_PACKAGE_NAME,
  ORDER_SERVICE_NAME,
} from '../../../proto/generated/order.interface';

@Module({
  imports: [
    // 初始化consul
    ConsulModule.forRoot({
      consulHost: 'http://127.0.0.1:8500',
      token: '123456',
    }),
    // 从consul中获取注册的微服务
    GrpcModule.registerClient([
      {
        injectToken: USER_PACKAGE_NAME,
        serviceName: USER_SERVICE_NAME,
        packageName: USER_PACKAGE_NAME,
        protoPath: 'user.proto',
      },
      {
        injectToken: ORDER_PACKAGE_NAME,
        serviceName: ORDER_SERVICE_NAME,
        packageName: ORDER_PACKAGE_NAME,
        protoPath: 'order.proto',
      },
    ]),
    UserModule,
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

然后启动服务调用就好

git更改commit