nestjs微服务搭建
对于 nestjs 的微服务项目,官方文档并不是很好理解,而且比较简单,因此想从零到一搭建一个完整的 nestjs 微服务,通过 gRPC
进行服务调用, 利用 consul
进行微服务的注册与发现。
为了方便管理,本项目采用 Monorepo
的方式组织代码。
整体介绍
项目结构
.
├── proto # 存储proto文件
├── scripts # 存储服务脚本
├── consul
│ ├── config
│ │ └── consul.hcl # consul配置文件
│ ├── data
│ └── docker-compose.yml
├── apps
│ ├── gateway # 网关服务
│ │ ├── src
│ │ │ ├── app.module.ts
│ │ │ └── main.ts
│ │ ├── test
│ │ │ ├── app.e2e-spec.ts
│ │ │ └── jest-e2e.json
│ │ └── tsconfig.app.json
│ ├── order-service # 订单服务
│ │ ├── src
│ │ │ ├── app.module.ts
│ │ │ ├── main.ts
│ │ │ └── user
│ │ │ ├── user.controller.ts
│ │ │ ├── user.module.ts
│ │ │ └── user.service.ts
│ │ ├── test
│ │ │ ├── app.e2e-spec.ts
│ │ │ └── jest-e2e.json
│ │ └── tsconfig.app.json
│ └── user-service # 用户服务
│ ├── src
│ │ ├── app.module.ts
│ │ ├── main.ts
│ │ └── user
│ │ ├── user.controller.ts
│ │ ├── user.module.ts
│ │ └── user.service.ts
│ ├── test
│ │ ├── app.e2e-spec.ts
│ │ └── jest-e2e.json
│ └── tsconfig.app.json
├── eslint.config.mjs
├── nest-cli.json
├── package-lock.json
├── package.json
├── README.md
├── tsconfig.build.json
└── tsconfig.json
调用流程
HTTP 网关(或 BFF 层)」作为统一入口,接收前端请求,然后它通过 gRPC 调用各个后端微服务(如user-service, order-service),拿到结果后统一封装返回给客户端。
浏览器 / App
↓ HTTP 请求
┌──────────────────┐
│ API Gateway (HTTP)│ ← NestJS,使用 Controller
└──────────────────┘
↓ 调用 gRPC 微服务
┌────────────┐ ┌──────────────┐
│ user-svc │ │ order-svc │
└────────────┘ └──────────────┘
初始化创建
首相创建项目基本结构,并下载相关依赖包。
nest new micro-service # 创建一个项目
cd micro-service
nest generate app user-service --no-spec # 创建 user 服务
修改项目中的 nest-cli.json
文件:
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "apps",
"root": ".",
"compilerOptions": {
"deleteOutDir": true,
"webpack": true
},
"monorepo": true,
"projects": {
"gateway": {
"type": "application",
"root": "apps/gateway",
"entryFile": "main",
"sourceRoot": "apps/gateway/src",
"compilerOptions": {
"tsConfigPath": "apps/gateway/tsconfig.app.json"
}
}
}
}
nest generate app order-service --no-spec # 创建 order 服务
nest generate app user-service --no-spec # 创建 order 服务
# proto
mkdir proto scripts
执行上面命令,总共创建三个服务
- gateway 服务:API网关层, 负责接收外部请求,可以统一校验Token、OAuth等身份认证信息,并根据用户角色或权限对访问进行控制。
- user-service 服务:用户相关的服务
- order-service 服务:订单相关的服务
- proto
- scripts
调整项目结构,在每个微服务下面创建一个 app.module.ts
, 然后创建对应的 resource
。
nest g res user --project user-service --no-spec
nest g res order --project order-service --no-spec
最终项目结构应和上述给出的结构一致。
修改服务端口号,此项目分别为:
- gateway: 3000
- user-service: 3001
- order-service: 3002
运行
分别执行下列命令启动项目:
npm run start:dev gateway
npm run start:dev user-service
npm run start:dev order-service
微服务改造
安装依赖包
npm i --save @nestjs/microservices # nestjs微服务包
npm i --save @grpc/grpc-js @grpc/proto-loader # grpc相关的包
npm i --save @grpc/reflection # 让 gRPC 服务具有自描述能力,便于调试和动态客户端接入,类似 REST 中的 Swagger。
npm install -D ts-proto # 用于通过 proto 文件生成 ts
增加proto文件
在增加 proto 文件之前,首先 在nest-cli.json
进行配置,添加assets
允许分发非 TypeScript 文件的属性,并watchAssets
启用了对所有非 TypeScript 资源的监控。在本例中,希望.proto
文件自动复制到该dist
文件夹
{
"compilerOptions": {
"assets": ["**/*.proto"],
"watchAssets": true
}
}
user.proto
syntax = "proto3";
package user;
service UserService {
rpc Create (User) returns (CreateOrUpdateResponse);
rpc FindAll (GetUserRequest) returns (GetUserResponse);
rpc FindOne (GetUserByIdRequest) returns (GetUserByIdResponse);
rpc Update (UpdateUserRequest) returns (CreateOrUpdateResponse);
rpc Remove (RemoveUserByIdRequest) returns (RemoveUserByIdResponse);
}
message CreateOrUpdateResponse {
User user = 1;
}
message GetUserRequest {}
message GetUserResponse {
repeated User users = 1;
}
message GetUserByIdRequest {
int32 id = 1;
}
message GetUserByIdResponse {
User user = 1;
}
message UpdateUserRequest {
int32 id = 1;
UpdateUserPayload payload = 2;
}
message UpdateUserPayload {
optional string password = 1;
optional string name = 2;
}
message RemoveUserByIdRequest {
int32 id = 1;
}
message RemoveUserByIdResponse {
User user = 1;
}
message User {
int32 id = 1;
string username = 2;
string password = 3;
string name = 4;
int32 age = 5;
}
order.proto
syntax = "proto3";
package order;
service OrderService {
rpc Create(Order) returns (CreateOrderResponse);
rpc FindOrderByUser(GetOrderByUserIdRequest) returns (GetOrderByUserIdResponse);
}
message CreateOrderResponse {
Order order = 1;
}
message GetOrderByUserIdRequest {
int32 id = 1;
}
message GetOrderByUserIdResponse {
repeated Order order = 1;
}
//枚举消息类型,使用enum关键词定义
enum OederStatus {
UNKNOWN = 0; // proto3版本中,首成员必须为0,默认值,通常表示未知或未指定
DOING = 1;
CACEL = 2;
DONE = 3;
REFUND = 4;
};
message Order {
int32 id = 1;
string Order = 2;
OederStatus status = 3;
}
proto文件生成 ts 接口
- 在
proto
文件夹下创建generated
文件夹。 - 在项目
scripts
文件夹,增加 proto 转 ts 文件的脚本gen-proto.ts
import { execSync } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
const protoDir = path.resolve(__dirname, '../proto');
const outputDir = path.resolve(__dirname, '../proto/generated');
const grpcMetadataParams = true;
// 自动查找 proto 目录下所有 .proto 文件
const protoFiles = fs.readdirSync(protoDir)
.filter(file => file.endsWith('.proto'))
.map(file => path.join(protoDir, file))
.join(' ');
// ts-proto 插件位置
// ts-proto关于 Nest的扩展文档 https://github.com/stephenh/ts-proto/blob/main/NESTJS.markdown
const pluginPath = path.resolve(__dirname, '../node_modules/.bin/protoc-gen-ts_proto');
// ts-proto 的生成参数
const tsProtoOptions = [
'nestJs=true', // 生成符合 NestJS 风格的 interface 和服务定义(如 @nestjs/microservices 用法)
'outputClientImpl=false', // 表示使用 grpc-web 风格的 client
'outputEncodeMethods=false', // 不生成 encode 方法(如 MyMessage.encode())
'outputJsonMethods=false', // 不生成 fromJSON, toJSON 方法
'outputPartialMethods=false', // 允许从一个部分字段的对象快速构造出一个完整合法的 protobuf 消息对象,自动为缺失的字段填上默认值
'outputTypeRegistry=false', // 不生成类型注册代码
'addNestjsRestParameter=true', // 服务方法的最后一个参数将是一个任意类型的剩余参数
].join(',');
// 最终命令
const cmd = [
'protoc',
`--plugin=${pluginPath}`,
`--ts_proto_opt=addGrpcMetadata=${grpcMetadataParams}`, //是否生成grpc调用时设置metadata的形参
`--ts_proto_out=${outputDir}`,
`--ts_proto_opt=${tsProtoOptions}`,
`--proto_path=${protoDir}`,
protoFiles
].join(' ');
console.log('Executing:', cmd);
execSync(cmd, { stdio: 'inherit' });
fs.readdirSync(outputDir)
.filter(f => f.endsWith('.ts') && !f.endsWith('.interface.ts'))
.forEach(file => {
const src = path.join(outputDir, file);
const dest = path.join(outputDir, file.replace(/.ts$/, '.interface.ts'));
fs.renameSync(src, dest);
console.log(`✅ 重命名 ${file} → ${path.basename(dest)}`);
});
- 在
package.json
文件中,增加 proto 文件转 ts 文件的运行命令,并执行命令生成 ts 文件。
{
"scripts": {
"gen:proto": "ts-node scripts/gen-proto.ts"
}
}
- 运行生成 ts 文件
npm run gen:proto
git仓库源码
实现
main函数改造
项目入口文件 main.ts
的书写方式有两种,方式一如下:
import { NestFactory } from "@nestjs/core";
import { MicroserviceOptions, Transport } from "@nestjs/microservices";
import { AppModule } from "./app.module";
import { USER_PACKAGE_NAME } from "proto/generated/user.interface";
import { join } from "path";
import { ReflectionService } from "@grpc/reflection";
async function bootstrap() {
const port = 3001;
const app = await NestFactory.create(AppModule);
app.connectMicroservice<MicroserviceOptions>({
transport: Transport.GRPC,
options: {
url: `0.0.0.0:${port}`,
package: USER_PACKAGE_NAME,
protoPath: join(__dirname, '../../../proto/user.proto'),
onLoadPackageDefinition: (pkg, server) => {
new ReflectionService(pkg).addToServer(server);
}
}
})
await app.startAllMicroservices();
await app.listen(port);
console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();
方式二:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Transport } from '@nestjs/microservices';
import { ORDER_PACKAGE_NAME } from 'proto/generated/order.interface';
import { join } from 'path';
import { ReflectionService } from '@grpc/reflection';
async function bootstrap() {
const port = 3002;
const grpcUrl = `0.0.0.0:${port}`;
const app = await NestFactory.createMicroservice(AppModule, {
transport: Transport.GRPC,
options: {
url: grpcUrl, //表示 gRPC 服务器监听的地址和端口。
package: ORDER_PACKAGE_NAME,
protoPath: join(__dirname, '../../../proto/order.proto'),
onLoadPackageDefinition: (pkg, server) => {
new ReflectionService(pkg).addToServer(server);
}
}
});
console.log(`Application is running on: ${grpcUrl}`);
}
bootstrap();
主要区别在于方式一先是创建一个 http 服务,在挂载一个微服务,也就是同时支持 http 服务和微服务两种方式调用。而方式二只支持微服务调用。
proto中的调用方法逻辑实现
这里就是具体我们要做的业务逻辑实现了
在 User.controller.ts
中,实现 user.proto 定义的 4 个增删改查的方法。下面只是例子,需要注意的是,实现的方法需要使用 GrpcMethod
装饰器进行装饰,第一个参数是在 proto 中 service
后定义的这个值
第二个参数为方法名,如何 proto 定义的方法名称和实现的方法名称一样,可以不写。
export class UserController {
constructor(private readonly userService: UserService) {}
@GrpcMethod('UserService')
Create(@Payload() createUserDto: User) {
const user = this.userService.create(createUserDto);
return { user };
}
@GrpcMethod('UserService', 'Update')
Update(@Payload() updateUserDto: UpdateUserRequest) {
const user = this.userService.update(
updateUserDto.id,
updateUserDto.payload!,
);
return { user };
}
而其中的 User
, UpdateUserRequest
都是从 proto 生成的 ts 文件中进行引入的。
git仓库源码
gateway 调用
首先在 gateway 中,创建 user module.
nest g res user --project gateway --no-spec
然后在 user.module.ts
中配置需要调用的微服务
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { join } from 'path';
import { ORDER_PACKAGE_NAME } from '../../../../proto/generated/order.interface';
import { USER_PACKAGE_NAME } from '../../../../proto/generated/user.interface';
import { UserController } from './user.controller';
import { UserService } from './user.service';
@Module({
imports: [
ClientsModule.register([
{
name: USER_PACKAGE_NAME,
transport: Transport.GRPC,
options: {
url: '0.0.0.0:3001',
package: USER_PACKAGE_NAME,
protoPath: join(__dirname, '../../../proto/user.proto'),
},
},
{
name: ORDER_PACKAGE_NAME,
transport: Transport.GRPC,
options: {
url: '0.0.0.0:3002',
package: ORDER_PACKAGE_NAME,
protoPath: join(__dirname, '../../../proto/order.proto'),
},
},
]),
],
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
然后在 user.service.ts
中进行服务初始化和调用
import { Metadata } from '@grpc/grpc-js';
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { firstValueFrom } from 'rxjs';
import {
ORDER_PACKAGE_NAME,
ORDER_SERVICE_NAME,
OrderServiceClient,
} from '../../../../proto/generated/order.interface';
import {
USER_PACKAGE_NAME,
USER_SERVICE_NAME,
UserServiceClient,
} from '../../../../proto/generated/user.interface';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Injectable()
export class UserService implements OnModuleInit {
private userService: UserServiceClient;
private orderService: OrderServiceClient;
constructor(
@Inject(USER_PACKAGE_NAME) private readonly userClient: ClientGrpc,
@Inject(ORDER_PACKAGE_NAME) private readonly orderClient: ClientGrpc,
) {}
onModuleInit() {
this.userService = this.userClient.getService<UserServiceClient>(USER_SERVICE_NAME);
this.orderService = this.orderClient.getService<OrderServiceClient>(ORDER_SERVICE_NAME);
}
async create(createUserDto: CreateUserDto) {
const user = this.userService.create(createUserDto);
return await firstValueFrom(user);
}
async findAll() {
const users = this.userService.findAll({});
return await firstValueFrom(users);
}
async findOne(id: number) {
const metadata = new Metadata();
metadata.add('key', 'w3423');
const user = this.userService.findOne({ id: +id }, metadata);
return await firstValueFrom(user);
}
async update(id: number, updateUserDto: UpdateUserDto) {
const user = this.userService.update({ id, payload: updateUserDto });
return await firstValueFrom(user);
}
async remove(id: number) {
const user = this.userService.remove({ id });
return await firstValueFrom(user);
}
async getUserOrder(id: number) {
const order = this.orderService.findOrderByUser({ id });
return await firstValueFrom(order);
}
}
需要注意的是,微服务返回的是一个 Observable
,所以需要将返回的结果调用 firstValueFrom
方法进行转换成对象。
git仓库源码
系列文章2:juejin.cn/post/752270…