封装独立的 GRPC 模块
通过之前的两篇文章,我们已经搭建起来 nestjs 的微服务项目。并使用 consul 作为注册中心,并封装了 consul 模块,提供了服务注册的方法和通过服务名获取服务的方法。
此篇文章,我将要创建一个 GrpcModule,它应该具备以下能力:
- 服务端能力: 能够轻松地将一个 NestJS 应用注册为 gRPC 服务。
- 客户端能力: 提供一个动态的
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)。这个提供者会:
- 注入您已经封装好的
ConsulService。 - 使用
ConsulService的resolveAddress方法来获取服务的address和port。 - 使用 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