一、开始之前
概念熟悉:
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
命令。
--path
指定proto
文件路径--output
指定输出ts
文件路径- 是否将
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
以其优秀的性能可跨端性等等,支持了众多的语言。希望这篇文章能够帮助到读者。