
还是 Nest.js,但这一次是微服务
故事的背景是,我要给我的“黑米说”系列工具增加各种各样的功能,目前设计了三个子项目:
子项目 | 说 明 | 域 名 | 备 注 |
---|---|---|---|
黑米说音乐厅 | 一个分享乐谱和演奏视频的网站 | 开发中 | |
黑米说图书馆 | 实物书籍整理借阅系统 | 开发中 | |
黑米说实验室 | 开源/自研软件授权平台 | 开发中 |
敲重点:实际上以上项目都是独立的项目,但是我希望他们可以通过一个体系进行整合和管理,那么微服务其实是一个不错的选择,但是不代表所有的这种情况都适合微服务。因此在技术选型方面,大家还是要根据自己的实际情况来进行选择。
Why 微服务?
我们以前写服务端,基本上就是在一个应用里放很多个模块,用模块来区分功能。如果在设计模块的时候把结构区分好,它的优点是很明显的:结构清晰,容易维护。
但是当我们的服务端模块越来越多的时候,你就会发现原本清晰的结构东拆西散,变得不那么清晰,甚至会有些混乱。他就变得不那么好维护了。
所以当我们在重构一个大型项目的时候往往会选择微服务,把功能接近的模块放在一起形成一个微服务,而统一使用一个出口吞吐数据。这样当其中一个微服务在维护的时候,整个项目不需要完全停机。尤其是在出现阻塞性缺陷时不会全部挂掉产生生产事故。
所以这次对以上项目重构的时候就使用了微服务架构。
Why Nest.js?
习惯了。
坦白讲 Nest.js 确实好用,虽然它一直以来在国内都不火。
在和其他的可能没有在继续维护的 node.js 企业级服务端框架相比,Nest.js 一直在维护,而且它的装饰器写法、模块化架构会让代码变得更加清晰,很中我这种推崇“代码自解释”的人的胃口。
项目架构
为了方便理解,在这里统一解释以下一些称呼的指代,获取与专业的后端研发不一致,也请专业人士不吝赐教:
网关:指的是那个负责统一出口的 HTTP 服务,Nginx 指向的就是这个运行中的服务;
服务:指的是负责某一项功能的微服务。
值得一提的是,Nest.js 的网关和服务之间的通信是使用 TCP 的,这和一般的微服务架构是有些区别的。
这次起服务的主要目的是重构“黑米说”系列工具的服务,所以下面的例子也都是以“黑米说”相关命名。
准备工作
首先确定自己的电脑上已经全局安装了 nest.js
nest --version
如果已经安装了 nest.js,将会显示版本号。我一般会建议把 node.js
和 nest.js
都升级到最新的版本。
如果还没有安装 nest.js,那么就先全局安装(注意权限):
npm install -g @nestjs/cli
接下来就可以准备起项目了。
网关
首先先起一个普通的 nest.js 的项目:
nest new mitkimi-core-gateway
如果你喜欢的话,可以和我一样使用 yarn
做包管理。
项目起好了以后,给项目安装依赖,然后再安装 @nestjs/microservices
包。
cd mitkimi-core-gateway
yarn
yarn add @nestjs/microservices
网关将会成为整个微服务架构的唯一出口,在使用方面也最接近于一个小型的 nest.js 应用。
这里我们先起好服务,并且装好依赖。“在网关中注册微服务”中会改造它。
yarn start:dev # 记得使用 dev,会惹更新
服务
先起一个普通的 nest.js 服务,然后也安装 @nestjs/microservices
依赖:
nest new mitkimi-service-user
cd mitkimi-service-user
yarn
yarn add @nestjs/microservices
安装好以后,打开 main.ts
,修改成下面的样子:
import { NestFactory } from '@nestjs/core';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.TCP,
options: {
port: 60001,
},
},
);
app.listen();
}
bootstrap();
如果是全新启动的项目,直接贴上面的代码替换原来的 main.ts
文件代码就可以。下面简单说明一下都有哪些改变:
首先从 @nestjs/microservices
依赖中引入了 Transport
和 MicroserviceOptions
,并且 app
创建成了微服务模式;Transport 是用来和网关做通信的,下面的 options.port
指定了当前服务使用的端口,这里是 60001。原则上这里的端口号可以是 0 ~ 65535 之间任意一个不和现有应用冲突的端口号,为了不和其他应用冲突和方便管理,我的所有微服务使用的端口从 60001 依次向下排。
接着启动项目开发模式
yarn start:dev
然后我们要在网关的 AppController 里注册如何处理来自网关的 TCP 消息,打开 app.controller.ts
文件,做以下修改:
在 @nestjs/microservices
中导出并引入 MessagePattern
,我们使用它来声明要处理的消息。
这里使用
MessagePattern
是因为我们需要把处理完的信息响应给网关。如果只处理事件不需要响应给网关(例如写日志),则可以使用
EventPattern
,在网关中使用 emit 来调用(这个就不举例了)。
import { Controller, Get } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
@Controller()
export class AppController {
constructor() {}
@MessagePattern('data')
SayHello(name: string): string {
return `Hello from user.microservice ${name}`
}
}
SayHello 方法表示把传来的 name 拼接进字符串中再返回回去。这一段用以模拟一个返回,实际上应该是 controller 到 service 的处理,然后再返回结果。
网关则是通过会通过 MessagePattern
装饰器找到 data 这个 SayHello 的方法,然后把相关的数据传过来,就可以在微服务中处理了。
在网关中注册微服务
在网关的 app.module.ts
文件中做以下改造:
首先从 @nestjs/microservices
依赖中引入了 Transport
和 ClientModules
,然后在 @Module 的 imports 里使用 ClientsModule.register
注册微服务,给微服务命名(非常重要,会用到)和指定当前注册的服务使用的端口。
每次有新增的微服务时都需要在网关中注册才能使用。
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ClientsModule, Transport } from '@nestjs/microservices';
@Module({
imports: [
ClientsModule.register([
{
name: 'USER_SERVICE',
transport: Transport.TCP,
options: {
port: 60001,
},
},
]),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
然后就可以在网关的 controller 里唤起微服务中的数据处理了。
需要在 @nestjs/microservices
中导出并引用 ClienProxy
并且在构造函数入参中 Inject 要使用的微服务的名称(刚才提到很重要的那个名称)并且实例化一个 ClientProxy,然后正常使用 Get 或者 Post 方法执行函数,和一般的 nest.js 项目写法一样。
然后在 controller 方法中调用这个实例的 send 函数,第一个参数是微服务中的 MessagePattern,后面是要发送到这个 MessagePattern 的参数。
例子中是在 app.controller.ts
中做的调用,实际开发时则需要在对应的 controller 中调用:
import { Controller, Get, Inject, Query } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { Observable } from 'rxjs';
@Controller()
export class AppController {
constructor(@Inject('USER_SERVICE') private userClient: ClientProxy) {}
@Get('/hello')
sayHello(
@Query('name') name
): Observable<string> {
const nextName = name || 'anonymous'
console.log('your name: ', nextName)
return this.userClient.send('data', nextName);
}
}
call
打开浏览器,进入 http://localhost:3000/hello?name=Kimi
得到这个页面:
控制台得到这个(包含了网关和微服务控制台):
你的基于 Nest.js 的微服务架构就基本上搭好了。
总结
这篇文章只是从搭建的角度去实现了一个 Nest.js 微服务架构。在正式的开发中,包含网关和服务在内的每个 nest.js 项目都需要单独去做一些后续的处理。
后面我会在代码磨练的比较好的时候开源一套模板项目,方面后续开发和交流学习。