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

相关推荐
一城烟雨_22 分钟前
vue3 实现将html内容导出为图片、pdf和word
前端·javascript·vue.js·pdf
raoxiaoya26 分钟前
同时安装多个版本的golang
开发语言·后端·golang
树懒的梦想1 小时前
调整vscode的插件安装位置
前端·cursor
低代码布道师2 小时前
第二部分:网页的妆容 —— CSS(下)
前端·css
考虑考虑2 小时前
go使用gorilla/websocket实现websocket
后端·程序员·go
一纸忘忧2 小时前
成立一周年!开源的本土化中文文档知识库
前端·javascript·github
李少兄2 小时前
解决Spring Boot多模块自动配置失效问题
java·spring boot·后端
涵信3 小时前
第九节:性能优化高频题-首屏加载优化策略
前端·vue.js·性能优化
前端小巷子3 小时前
CSS单位完全指南
前端·css
Piper蛋窝3 小时前
Go 1.19 相比 Go 1.18 有哪些值得注意的改动?
后端