NestJS gRPC 微服务如此简单

一、开始之前

概念熟悉:

  • NestJS: 微服务
  • gRPC proto 文件 => TS 声明文件
  • NestJSRxJS: 可观察对象

二、Why NestJS and gRPC?

  • 适用于微服务架构
  • 多种语言支持
  • 性能优异

三、依赖安装

sh 复制代码
pmpm add @nestjs/microservices 
pnpm add rxjs 
pnpm add nestjs-proto-gen-ts # 命令行
pnpm add @grpc/proto-loader @grpc/grpc-js # gRPC 解析

四、定义你的 proto 文件

以下是一个 link 标签增删改查历列表数量的 proto 文件

ts 复制代码
syntax = "proto3";

package link;
service LinkService {
  rpc CreateLink(CreateLinkRequest) returns (LinkResponse);
  rpc DeleteLink(DeleteLinkRequest) returns (LinkResponse);
  rpc UpdateLink(UpdateLinkRequest) returns (LinkResponse);
  rpc GetLink(GetLinkRequest) returns (LinkResponse);
  rpc ListLinks(ListLinksRequest) returns (ListLinksResponse);
  rpc GetLinksCount(GetLinksCountRequest) returns (GetLinksCountResponse);
}
message Link {
  string id = 1;
  string name = 2;
  string desc = 3;
  string url = 4;
  string overUrl = 5;
}
message CreateLinkRequest {
  string name = 1;
  string desc = 2;
  string url = 3;
  string overUrl = 4;
}
message DeleteLinkRequest {
  string id = 1;
}
message UpdateLinkRequest {
  string id = 1;
  string desc = 2;
  string url = 3;
  string overUrl = 4;
}
message GetLinkRequest {
  string id = 1;
}
message LinkResponse {
  Link link = 1;
}
message ListLinksRequest {
  int32 page = 1;
  int32 pageSize = 2;
}
message ListLinksResponse {
  repeated Link links = 1;
}
message GetLinksCountRequest {}
message GetLinksCountResponse {
  int32 count = 1;
}

这个 proto 文件有 两用:客户端和服务端都要用到。最好的办法是将 proto 单独的抽象到一个库中,然后本地引用

五、将 proto 文件变成 TS 类型

  • 定义脚本
sh 复制代码
{
    "nest-proto": "tsproto --path ./src/proto/ --output ./src/types",
}
  • 输出结果

nestjs-proto-gen-ts 包提供了 tsproto 命令。

  1. --path 指定 proto 文件路径
  2. --output 指定输出 ts 文件路径
  3. 是否将 proto 抽象为一个 pnpm 包,提供给不同的微服务使用。
  • 输出的声明文件
ts 复制代码
import { Observable } from 'rxjs';
import { Metadata } from '@grpc/grpc-js';

export namespace link {
    export interface LinkService {
        createLink(
            data: CreateLinkRequest,
            metadata?: Metadata,
            ...rest: any[]
        ): Observable<LinkResponse>;
        deleteLink(
            data: DeleteLinkRequest,
            metadata?: Metadata,
            ...rest: any[]
        ): Observable<LinkResponse>;
        updateLink(
            data: UpdateLinkRequest,
            metadata?: Metadata,
            ...rest: any[]
        ): Observable<LinkResponse>;
        getLink(
            data: GetLinkRequest,
            metadata?: Metadata,
            ...rest: any[]
        ): Observable<LinkResponse>;
        listLinks(
            data: ListLinksRequest,
            metadata?: Metadata,
            ...rest: any[]
        ): Observable<ListLinksResponse>;
        getLinksCount(
            data: GetLinksCountRequest,
            metadata?: Metadata,
            ...rest: any[]
        ): Observable<GetLinksCountResponse>;
    }
    export interface Link {
        id?: string;
        name?: string;
        desc?: string;
        url?: string;
        overUrl?: string;
    }
    export interface CreateLinkRequest {
        name?: string;
        desc?: string;
        url?: string;
        overUrl?: string;
    }
    export interface DeleteLinkRequest {
        id?: string;
    }
    export interface UpdateLinkRequest {
        id?: string;
        desc?: string;
        url?: string;
        overUrl?: string;
    }
    export interface GetLinkRequest {
        id?: string;
    }
    export interface LinkResponse {
        link?: link.Link;
    }
    export interface ListLinksRequest {
        page?: number;
        pageSize?: number;
    }
    export interface ListLinksResponse {
        links?: link.Link[];
    }
    export interface GetLinksCountRequest {
    }
    export interface GetLinksCountResponse {
        count?: number;
    }
}

特点:

  • 存在命名空间就是 proto 文件的 package.
  • 输出的属性是可选的.
  • 输出的方法,方法的返回值是可观察对象(这里特别重要,不是 promise).

六、 gRPC 微服务

HTTP 服务中套 gRPC 微服务,

ts 复制代码
const app = await NestFactory.create(AppModule);
app.connectMicroservice({
  transport: Transport.GRPC,
  options: {
    url: 'http://localhost:50000',
    package: 'link',
    protoPath: join(__dirname, 'protos/link.proto'),
  },
});
app.startAllMicroservices();

app.listen(3000, () => { /* */ })

此处,微服务与 http 服务一起启动,也可以单独的将启动。

七、创建对外提供 NestJS gRPC 接口

ts 复制代码
import { Controller } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';

// services
import { MicroService } from './micro.service';

// dtos
import * as dtos from '@/modules/v1/app/dtos';

@Controller()
export class MicroController {
  constructor(private readonly microService: MicroService) {}

  @GrpcMethod('LinkService', 'CreateLink')
  async create(createCategoryDto: dtos.CreateCategoryDto) {
    return await this.microService.create(createCategoryDto);
  }

  @GrpcMethod('LinkService', 'DeleteLink')
  async del(id: string | { id: string }) {
    return this.microService.del(typeof id === 'string' ? id : id.id);
  }

  @GrpcMethod('LinkService', 'UpdateLink')
  async update(updateCategoryDto: dtos.UpdateCategoryDto) {
    return this.microService.update(updateCategoryDto);
  }

  @GrpcMethod('LinkService', 'GetLink')
  async find(id: string | { id: string }) {
    if (typeof id === 'string') {
      return this.microService.get(id);
    } else {
      return this.microService.get(id.id);
    }
  }

  @GrpcMethod('LinkService', 'ListLinks')
  async list({ page, pageSize }: { page: number; pageSize: number }) {
    return await this.microService.list({
      page: page ?? 1,
      pageSize: pageSize ?? 10,
    });
  }

  @GrpcMethod('LinkService', 'GetLinksCount')
  async count() {
    return await this.microService.count();
  }
}

到此为之,服务端的代码就完成了, 启动服务端服务,就可以在访问 gRPC 服务了。

八、调试 gRPC 接口

在写客户端代码之前,我们可以借助工具先测试 gPRC 接口,这里使用的工具是:BloomRPC

九、gGRC 模块注册到客户端

  • Module异步注册 Link 微服务
ts 复制代码
import { join } from 'path';
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConsulService } from '@/services';
import { AppConfigService } from '@/modules/v1/config/config.service';
const protoPath = '../../../../node_modules/shared-proto/src/proto';

@Module({
  imports: [
    ClientsModule.registerAsync(
      {
        name: 'MICRO_GRPC_LINK',
        imports: [AppConfigService],
        useFactory: async (appConfigService: AppConfigService) => {
          const { host, port, pkg } = appConfigService.get('app.gRPCLink');
          return {
            name: 'MICRO_GRPC_LINK',
            transport: Transport.GRPC,
            options: {
              url: `${host}:${port}`,
              package: pkg,
              protoPath: join(__dirname, protoPath, pkg, '.proto'),
            },
          };
        },
      },
    ]),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
  • Service 注入 Link Client, 然后使用
ts 复制代码
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { forkJoin, map } from 'rxjs';
import { link } from 'shared-proto'; // proto 生成单独的文件声明文件,被单独的抽象到一个 pnpm 包里面。

@Injectable()
export class AppService implements OnModuleInit {
  private linkService: link.LinkService;


  constructor(
    @Inject('MICRO_GRPC_LINK') private linkClient: ClientGrpc,
  ) {}

  onModuleInit() {
    this.linkService =
      this.linkClient.getService<link.LinkService>('LinkService');
  }

  getData() {
    const linkCount = this.linkService.GetLinksCount({});
    const linkList = this.linkService.ListLinks({
      page: 1,
      pageSize: 10,
    });


    return forkJoin([
         // ..
      linkCount,
      linkList,
        // ...
    ]).pipe(
      map(
        ([
          // ...
          _linkCount,
          _linkList,
          // ...
        ]: any) => {
          return {
               // ...
            link: {
              count: _linkCount.count,
              list: _linkList.links,
            },
              // ...
          };
        },
      ),
    );
  }
}

接口 controller 可以调用 getData 方法,通过 gRPC 的可观察对象 forkJoin 并发的获取到数据。值得注意的是, 全程使用 RxJS 的可观察对象作为类型,保证类型安全。这意味需要熟悉 RxJS

十、暴露 HTTP 接口

ts 复制代码
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getDashboard() {
    return this.appService.getData();
  }
}

通过此 HTTP 接口就可以访问到 gRPC 微服务。这就是整个过程,客户端与服务端的注册与使用过程。

十一、小结

本文以简单的实践代码,演示了一个 NestJS gRPC 微服务的服务端创建与客户端创建消费的过程。除去了 gRPC 我们还有 RxJS,这是 NestJS 内置的能力,使用 NestJS 必须掌握。 gRPC 以其优秀的性能可跨端性等等,支持了众多的语言。希望这篇文章能够帮助到读者。

相关推荐
bcbnb2 分钟前
Ipa Guard 集成到 CICD 流程,让 iOS 加固进入自动化时代的完整工程方案
后端
拉不动的猪8 分钟前
Axios 请求取消机制详解
前端·javascript·面试
该用户已不存在11 分钟前
2025 年 8 款最佳远程协作工具
前端·后端·远程工作
lxh011319 分钟前
螺旋数组题解
前端·算法·js
E***U94521 分钟前
前端安全编程实践
前端·安全
云渠道商yunshuguoji26 分钟前
阿里云渠道商:阿里云服务器出问题如何处理?
后端
老华带你飞31 分钟前
海产品销售系统|海鲜商城购物|基于SprinBoot+vue的海鲜商城系统(源码+数据库+文档)
java·前端·数据库·vue.js·论文·毕设·海鲜商城购物系统
x***B41141 分钟前
React安全编程实践
前端·安全·react.js
D***t1311 小时前
前端微服务案例
前端
哀木1 小时前
诶,这么好用的 mock 你怎么不早说
前端