NestJS小技巧34-用Monorepo来开发NestJS微服务:手把手指导
欢迎来到使用monorepo进行微服务开发的世界。在世界上的现代化软件,结合微服务和monorepo对于更加高效易于成长和代码复用的开发软件开始变得非常有用。
内容列表
第一步:将项目转成Monorepo
第二步:转化成微服务
第三步:配置通信模式
第四步:配置服务内部通信
第五步:调试微服务
第六步:运行所有的微服务
在开始进入如何在NestJS中利用微服务,我们需要先熟悉2个概念:
微服务:微服务是一个广泛被使用的架构旨在切分各种服务用于不同的业务目的。微服务的流行有很多理由,包括 解藕,更快的CI/CD,易于管理,和可伸缩。
Monorepo:monorepo是一个用于描述包含多个项目的仓库的术语。在我们的情况,一个仓库概括了所有我们的服务(微服务)并有一个Git仓库。
单体仓库相对于多仓库的优势:
- 统一开发:所有的东西都在一个地方,使工作更容易。
- 代码共享和复用:能够更容易分享和复用代码。
- 依赖管理:对于所有的服务,易于管理和分享所有您的依赖
- 简单的CI/CD
尽管有各种方法,如 Nx、TurboPack 等来创建单体仓库,但好消息是 NestJS 简化了创建和管理多个微服务的过程。而不依赖于第三方工具,NestJS 提供了对各种通信模式的原生支持。它还提供了使用 Nest CLI 独立运行和构建不同微服务的灵活性。
第一步: 将项目转成Monorepo
根据NestJS的文档,有2种方法来组织您的代码:
- 标准模式
- Monorepo
使用默认,使用NestClInest new new-project
创建项目会被设置为标准模式。您的初始化nest-cli.json
文件会有下面这些内容:
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}
为了使您的项目成为Monorepo模式,您需要在其中引入多个项目。主要有两种项目类型:
- Applications:这些是独立的NestJS项目。
- 创建应用程序:
nest generate app
- 创建应用程序:
- Libraries:这些包括在各个应用程序中使用的共享模块。
- 创建库:
nest generate library
- 创建库:
生成这些项目后,您的项目结构将如下所示:
📦NestJS_Project ┣ 📂apps ┃ ┗ 📂app1 ┃ ┃ ┣ 📂src ┃ ┃ ┃ ┣ 📜app1.controller.spec.ts ┃ ┃ ┃ ┣ 📜app1.controller.ts ┃ ┃ ┃ ┣ 📜app1.module.ts ┃ ┃ ┃ ┣ 📜app1.service.ts ┃ ┃ ┃ ┗ 📜main.ts ┃ ┃ ┗ 📜tsconfig.app.json ┣ 📂libs ┃ ┗ 📂lib1 ┃ ┃ ┣ 📂src ┃ ┃ ┃ ┣ 📜index.ts ┃ ┃ ┃ ┣ 📜lib1.module.ts ┃ ┃ ┃ ┣ 📜lib1.service.spec.ts ┃ ┃ ┃ ┗ 📜lib1.service.ts ┃ ┃ ┗ 📜tsconfig.lib.json ┣ 📜nest-cli.json ┣ 📜package.json ┗ 📜tsconfig.json
然后您的nest-cli.json
会像下面这样:
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "apps/app1/src",
"compilerOptions": {
"deleteOutDir": true,
"webpack": true,
"tsConfigPath": "apps/app1/tsconfig.app.json"
},
"monorepo": true,
"root": "apps/app1",
"projects": {
"app1": {
"type": "application",
"root": "apps/app1",
"entryFile": "main",
"sourceRoot": "apps/app1/src",
"compilerOptions": {
"tsConfigPath": "apps/app1/tsconfig.app.json"
}
},
"lib1": {
"type": "library",
"root": "libs/lib1",
"entryFile": "index",
"sourceRoot": "libs/lib1/src",
"compilerOptions": {
"tsConfigPath": "libs/lib1/tsconfig.lib.json"
}
}
}
}
第二步: 转化成微服务
在NestJS中创建微服务可能看起来像小菜一碟,但有些方面您应该留意。本质上,在NestJS中创建微服务有两种主要方法:
- 利用
NestFactory.createMicroservice()
:
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.TCP,
options: {
host: 'localhost',
port: 3001,
},
});
await app.listen();
虽然这种方法工作得很好,但它有两个缺点让我在完全采用它时犹豫不决:
- 使用createMicroservice方法创建的实例的类型是代替了
INestApp
的INestMicroservice
。这会在使用第三方包和库的时候受到限制。这是因为这些包只接收INestApp
的输入。 - 它只允许初始化单一的通信协议。例如,你只能使用一个特定的协议在一个端口上监听。
- 利用 connectMicroservice() 方法: 这种方法被用来创建一个更加灵活和强大的应用实例,在文档中通常被称为混合应用。与之前的方法相比,它的性能更好,提供了更高的灵活性。使用这种方法,你可以创建一个INestApp类型的应用,它允许你同时监听多个端口并使用多个传输器。
const app = await NestFactory.create(AppModule);
// microservice #1
const microserviceTcp = app.connectMicroservice<MicroserviceOptions>({
transport: Transport.TCP,
options: {
host: 'localhost',
port: 3001,
},
});
// microservice #2
const microserviceRedis = app.connectMicroservice<MicroserviceOptions>({
transport: Transport.REDIS,
options: {
host: 'localhost',
port: 6379,
},
});
await app.startAllMicroservices();
await app.listen(3002);
注意:当使用connectMicroservice方法并为HTTP和TCP指定相同的端口时,HTTP端口配置将被忽略,我们只会得到一个TCP监听器。 注意:重要的是要记住,如果你省略了主机参数或提供了一个错误的值(如null、undefined或空字符串),默认值将返回localhost的IPv6版本(::1)。
在完成了上述步骤以后,您可以使用NestCLI来启动单独的微服务。用这个命令:
nest start [app_name]
比如,在我们的例子中,您可以使用nest start app1
。
第三步:配置通信模式
在NestJS中,有两种不同类型的传输方式:
- 基于代理的:这一类包括NATS,以及其他选项如Redis、RabbitMQ、MQTT和Kafka。
- 点对点的:这一组包括TCP和gRPC通信机制。
为了配置您的传输方式,您需要在您的微服务设定中修改transport
参数。这里有个使用Redis
或者NATS
传输的例子:
#REDIS trasnporter
transport: Transport.REDIS
----------------------------
#NATS transporter
transport: Transport.NATS
第四步:配置服务内部通信
为了能够在多个服务之间进行通信,配置他们的各自的模型是很重要的并且我们在库中使用我们的服务。
配置响应服务器:
在我们把服务变成微服务后,我们必须对那个服务添加一个控制器:
import { EventPattern, MessagePattern, Payload } from '@nestjs/microservices';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
//基于代理控制器
@EventPattern('testEvent')
getHello(@Payload() data: any) {
return `Hello !`
}
//点对点控制器
@MessagePattern('testMessage')
getHelloTCP(name: string): string {
return `Hello !`;
}
}
NestJS提供了2种类型的消息模式,每一种都对应一个特别的通信需要:
@MessagePattern
:这个模式是为点对点传输定制的,在组件之间提供直接通信。
- 同步返回:控制器利用一个标准的返回类型。
@MessagePattern({ cmd: 'sum' })
accumulate(data: number[]): number {
return (data || []).reduce((a, b) => a + b);
}
- 异步返回:控制器返回一个Promise。
@MessagePattern({ cmd: 'sum' })
async accumulate(data: number[]): Promise<number> {}
- 流式返回:控制器返回Observable。这个方法需要具备一些RxJS的知识。
@MessagePattern({ cmd: 'sum' })
accumulate(data: number[]): Observable<number> {
return from([1, 2, 3]);
}
@EventPattern
:专门为基于代理传输而设计的,这个模式通过一个消息代理来促进通信。在这个例子中,我们的服务将会监听user_created
事件:
@EventPattern('user_created')
async handleUserCreated(data: Record<string, unknown>) {
// 业务逻辑
}
注意:我们可以为单个事件模型设计多个事件句柄,并且这些句柄同时运行。
配置请求服务
当我们设定请求服务(客户端)时,您需要对[app_name].modules.ts
文件做一些改变来注册应答服务。
@Module({
imports: [
ClientsModule.register([
{
name: 'AUTH_REDIS',
transport: Transport.REDIS,
options: {
client: {
clientId: 'auth',
brokers: ['localhost:6379'],
},
producerOnlyMode: true,
consumer: {
groupId: 'auth-consumer',
},
},
},
{
name: 'AUTH_TCP',
transport: Transport.TCP,
options: {
options: {
host: "localhost",
port: 3001
}
},
},
]),
],
providers: [AuthService],
controllers: [AuthController],
})
注意:库对请求方也同样有效,您需要对
[library_name].module.ts
文件以相同的方式修改他们.
下面是展示我们如何注入微服务和消费他:
import { Controller, Get , Inject} from '@nestjs/common';
@Controller()
export class AppController {
constructor(
@Inject('AUTH_REDIS') private redisClient: ClientProxy;
@Inject('AUTH_TCP') private tcpClient: ClientProxy
) {}
//TCP requester
@Get('call_tcp')
getTCP(data: any) {
return this.tcpClient.send('pattern_name' , data)
}
//Redis requester
@Get('call_redis')
getRedis(data: any) {
return this.redisClient.emit('event_name' , data)
}
}
注意: 为了注入模式,我们必须使用自定义provider,这是在 nestjs 中常用的技术,请阅读此链接以获取更多信息。 注意: 默认情况下,当客户端服务启动时(尽管我们可以使用 onApplicationBootstrap 钩子来实现这一点)连接并没有被创建,这个连接将会在第一次微服务被调用时被创建并被之后的调用继续使用(TCP频道会保持打开的状态)
第五步:调试微服务
当我们使用诸如Postman或者Insomnia来调试REST API,我们也需要工具来直接和微服务进行通信和调试。
在Nest服务之间设置通信是非常简单的,因为所有的东西都是设计为一体的,你可以轻松地导入你的目标微服务并调用它。
然而,如果一些您的服务用的是不同语言来构造的?您怎么来解决这个问题?
尽管深入探讨这个主题需要一篇独立的文章,但在这篇博客文章中,我将介绍如何识别TCP服务器的问题。不过,我也计划写另一篇博客文章,进一步探索这个有趣的话题 😉。
调用TCP微服务: 为了实现这个目的,我们需要一个工具它能发送和接收TCP数据包。满足这些需求,您可以使用的工具有Packet Sender 或者 Hercules。
为了测试,我们将会向这个控制器发送请求:
@MessagePattern('get_multiple'})
accumulate(data: number): number {
return data * 2;
}
我们的数据包将会具有基于ASCII的结构:
37#{"pattern":"get_multiple","data":"1"}
总的来说,我们传输的元数据格式为JSON。但是,在字符串的开头有一个额外的元素,标记为(37#
)。这里,37表示字符串中的字符数。您可以访问此链接来计算字符串的长度。
有2个方法来对应这个模式。第一种方式使用字符串输入,'create_user'
。第二种方法需要提供一个对象,比如{ cmd: 'create_user' }
。
基于这些解决方法,我们的数据包将会有不同的结构:
@MessagePattern('create_user') => "pattern":"create_user"
-------------------------------------
@MessagePattern({ cmd: 'create_user'}) => "pattern":{"cmd": "create_user"}
第六步:运行所有的微服务
通常,要并发运行多个微服务,您会为每个微服务进行Docker化,然后使用Docker Compose
将所有镜像整合在一起。然而,我通过一个bash脚本
开发了一个更简单的方法来实现相同的结果。
开始之前,您需要安装concurrency
作为dev依赖:
npm install concurrency --save-dev
现在使用这个bash脚本来同时启动您所有的微服务:
#!/bin/bash
initCMD='npx concurrently'
cyanColor="\033[0;36m"
for app in $(cd ./apps && ls); do
echo -e "${cyanColor}${app%/*} service starting ..."
initCMD+=" \"npx nest start ${app%/*}\""
done
eval "$initCMD"
# For not closing terminal if we face an error
read -p "Press any key to continue" x
谢谢大家来阅览我的文章,如果这篇文章对您们有帮助,请点赞和评论,您们的支持是我更新文章最大的动力。
资源
转载自:https://juejin.cn/post/7277084037070831651