故事的背景是,我要给我的“黑米说”系列工具增加各种各样的功能,目前设计了三个子项目:

子项目

说 明

域 名

备 注

黑米说音乐厅

一个分享乐谱和演奏视频的网站

music.mitkimi.com

开发中

黑米说图书馆

实物书籍整理借阅系统

library.mitkimi.com

开发中

黑米说实验室

开源/自研软件授权平台

lib.mitkimi.com

开发中

敲重点:实际上以上项目都是独立的项目,但是我希望他们可以通过一个体系进行整合和管理,那么微服务其实是一个不错的选择,但是不代表所有的这种情况都适合微服务。因此在技术选型方面,大家还是要根据自己的实际情况来进行选择。

Why 微服务?

我们以前写服务端,基本上就是在一个应用里放很多个模块,用模块来区分功能。如果在设计模块的时候把结构区分好,它的优点是很明显的:结构清晰,容易维护。

但是当我们的服务端模块越来越多的时候,你就会发现原本清晰的结构东拆西散,变得不那么清晰,甚至会有些混乱。他就变得不那么好维护了。

所以当我们在重构一个大型项目的时候往往会选择微服务,把功能接近的模块放在一起形成一个微服务,而统一使用一个出口吞吐数据。这样当其中一个微服务在维护的时候,整个项目不需要完全停机。尤其是在出现阻塞性缺陷时不会全部挂掉产生生产事故。

所以这次对以上项目重构的时候就使用了微服务架构。

Why Nest.js?

习惯了。

坦白讲 Nest.js 确实好用,虽然它一直以来在国内都不火。

在和其他的可能没有在继续维护的 node.js 企业级服务端框架相比,Nest.js 一直在维护,而且它的装饰器写法、模块化架构会让代码变得更加清晰,很中我这种推崇“代码自解释”的人的胃口。

项目架构

为了方便理解,在这里统一解释以下一些称呼的指代,获取与专业的后端研发不一致,也请专业人士不吝赐教:

网关:指的是那个负责统一出口的 HTTP 服务,Nginx 指向的就是这个运行中的服务;

服务:指的是负责某一项功能的微服务。

值得一提的是,Nest.js 的网关和服务之间的通信是使用 TCP 的,这和一般的微服务架构是有些区别的。

这次起服务的主要目的是重构“黑米说”系列工具的服务,所以下面的例子也都是以“黑米说”相关命名。

准备工作

首先确定自己的电脑上已经全局安装了 nest.js

nest --version

如果已经安装了 nest.js,将会显示版本号。我一般会建议把 node.jsnest.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 依赖中引入了 TransportMicroserviceOptions ,并且 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 依赖中引入了 TransportClientModules ,然后在 @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 得到这个页面:

result.png控制台得到这个(包含了网关和微服务控制台):

console-log.png你的基于 Nest.js 的微服务架构就基本上搭好了。

总结

这篇文章只是从搭建的角度去实现了一个 Nest.js 微服务架构。在正式的开发中,包含网关和服务在内的每个 nest.js 项目都需要单独去做一些后续的处理。

后面我会在代码磨练的比较好的时候开源一套模板项目,方面后续开发和交流学习。