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

相关推荐
小厂永远得不到的男人9 分钟前
基于 Spring Validation 实现全局参数校验异常处理
java·后端·架构
roamingcode1 小时前
Claude Code NPM 包发布命令
前端·npm·node.js·claude·自定义指令·claude code
码哥DFS1 小时前
NPM模块化总结
前端·javascript
灵感__idea2 小时前
JavaScript高级程序设计(第5版):代码整洁之道
前端·javascript·程序员
唐璜Taro2 小时前
electron进程间通信-IPC通信注册机制
前端·javascript·electron
陪我一起学编程3 小时前
创建Vue项目的不同方式及项目规范化配置
前端·javascript·vue.js·git·elementui·axios·企业规范
LinXunFeng4 小时前
Flutter - 详情页初始锚点与优化
前端·flutter·开源
GISer_Jing4 小时前
Vue Teleport 原理解析与React Portal、 Fragment 组件
前端·vue.js·react.js
Summer不秃4 小时前
uniapp 手写签名组件开发全攻略
前端·javascript·vue.js·微信小程序·小程序·html
毅航4 小时前
从原理到实践,讲透 MyBatis 内部池化思想的核心逻辑
后端·面试·mybatis