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

相关推荐
Canace1 分钟前
最新版 Codex 工作流的问题
前端·人工智能·agent
Lost of 程序猿2 分钟前
ASP.NET Core API 版本管理与契约演进:船岸版本碎片化下的兼容性工程
后端·asp.net
变与不变8064 分钟前
引用类型、内存原理与对象
开发语言·前端·javascript
jearry5 分钟前
给 C++ 老程序补现代 UI:WebView2 多入口资源加载与虚拟主机名实践
前端
打呵欠的猫9 分钟前
让 AI 帮你写 Git Commit Message:从"fix bug"到语义化提交只需一个 Hook
前端·ai编程
labixiong10 分钟前
CSS 锚点定位实测:零 JS 实现气泡跟随,自动翻转很香但暗藏 3 个致命坑
前端·css
qibmz10 分钟前
用 DeepSeek 给 GitHub 仓库加 PR 自动 Review
前端
计算机魔术师11 分钟前
幻觉率从 4.2% 降到 2% 又退回,GPT-6 Astra 到底在藏什么
前端
吃饱了得干活12 分钟前
Redis 从单机到集群:持久化、主从复制、哨兵与集群完全指南
redis·后端
梨想橙汁12 分钟前
Git 常用命令大全:提交、查看日志、版本回退、文件撤销
前端·javascript