NestJS gRPC 微服务如此简单

一、开始之前

概念熟悉:

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

二、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 以其优秀的性能可跨端性等等,支持了众多的语言。希望这篇文章能够帮助到读者。

相关推荐
ServBay1 小时前
Claude Fable 5.5 测试曝光,但它在悄悄给你打分
后端·ai编程·claude
孟健1 小时前
Claude Code 同需求写两版对比:装不装 frontend-design 差多远?
前端·ai编程·claude
IT规划师2 小时前
一文讲透五种发布机制:滚动、蓝绿、金丝雀、红黑与 AB
后端·面试
一条Go3 小时前
两套权限目录,三重闸门:Alioth 的多租户 RBAC 为什么这么拧
后端·程序员·github
Tokenfree3 小时前
把多模态 OCR 接进业务系统:四千张运单,三个坑
后端
梦de天空一米16883 小时前
【一个重复id让游戏卡在62%,聊聊配置表检查】
前端
ryn483983 小时前
01 一个功能模块,怎么分层才不乱
前端·ai编程·claude
用户859313061353 小时前
我给 AI Agent 写了个下载器:293KB 的网页,不再吃掉 7 万 token
后端
Amos_Web3 小时前
通过 Rspack 源码学 Rust(五):读懂 Result 与问号运算符的错误返回
前端
杨航 AI4 小时前
JEV的用法
java·前端·数据库