[一期 - 3] 这可能是你看过最全的 「NestJS」 教程了 - 统一返回 、 文件服务、 单点登录、Job、和部署

8,136 阅读18分钟

文章修正于 2024/01/16 (去掉错别字/把逻辑理更顺)

本文概要和目录

书接上文,我们继续来完善我们的这个Nest应用

重要提醒!:请不要照着文章照抄,建议你先阅读通篇,了解全貌之后再去实践。

image.png

统一返回体

我们回顾我们的目前的应用 不难发现,在返回体Res上我们比较混乱没有一致的返回格式,是时候改变了,让我们使用interceptor 改造他们

  • 新建一个拦截器去处理

我们默认你已经学习并且掌握了 Interceptor拦截器的所有知识点,如果你还不能掌握 请前往 我之前的文章

// http-req.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export interface Response<T> {
  data: T;
}

@Injectable()
export class HttpReqTransformInterceptor<T>
  implements NestInterceptor<T, Response<T>>
{
  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Observable<Response<T>> {
    return next
      .handle()
      .pipe(map((data) => ({ data, code: 200, msg: '', success: true })));
      // 我们非常的单纯把 返回体统一进行了包装 ,不要学我哈,我这里就是为了写而瞎写的,具体的返回code是什么。以及其他的内容 你需要判断,自己设计逻辑。
      // 落地到实际运用,你也许会从data中再剥离一部分属性 来构建这个标准的返回体,这完全取决于你的团队要求
  }
}

  • 如何使用

其他任何 Interceptor 一样去使用就好了,没有什么区别

@ApiTags('Tag相关')
@ApiBearerAuth()
@Controller('tag')
@UseInterceptors(new HttpReqTransformInterceptor<any>()) // 统一返回体
export class TagController {
  constructor(private readonly tagService: TagService) {}

  @UseGuards(AuthGuard('jwt'))
  @Get('/tags')
  async getAll() {
    const value = await this.tagService.getAll();
    return value;
  }

  @UseGuards(AuthGuard('local'))
  @Post()
  async createTag(@Body() tagInfo: Tag) {
    const value = await this.tagService.create(tagInfo);
    return value;
  }

  @Put('/:id')
  async updateTag(@Param() params: InterParams, @Body() tagInfo: Tag) {
    const value = await this.tagService.updateById(params.id, tagInfo);
    return value;
  }

  @Delete('/:id')
  async deleteTag(@Param() params: InterParams) {
    const value = this.tagService.deleteById(params.id);
    return value;
  }
}

最后你将会得到下面这样的返回

{
    "data": [],
    "code": 200,
    "msg": "",
    "success": true
}
  • 和Dto冲突了怎么办?认证 冲突了怎么办?

    细心的小伙伴。已经发现了上面的问题了,那就是,如果我没有验证通过或者我的引应用程序发送了错误🙅‍♂️ 出BUG啦,那么你将会得到下面的返回体.

    实际上哈,在正常的工作中,正常的res 和 异常的res 本身就是两套,我们的设计是没有问题的。下面只是说一下。当然你要统一格式的话也是可以的 去把异常的filler 改一下就好了,在此不赘述

// 我们故意不传递 正常的 验证信息
{
    "statusCode": 401,
    "error": "Unauthorized",
    "msg": "Client Error"
} 

// 写一个程序bug
{
    "statusCode": 500,
    "msg": "Service Error: Error: xxx"
}

// 我们期待的是
{
    "data": [],
    "code": 200,
    "msg": "",
    "success": true
}

// 这里的解决方案是把 制造这种异常的制造者干掉!,当然你也可以把你自己干掉,你就修改成和Nest系统内置的保存一样的接口类型就好了


// 首先是我们要处理所有的异常过滤器让他们变成我们所期待的样子
// 我们把所有的异常过滤器都修改成统一的返回数据结构体 

//AllExceptionsFilter
response.status(status).json({
      code: status,
      message: exception.message,
      data: null,
      success: false,
    });
  }
  
// HttpExceptionFilter
response.status(status).json({
  code: status,
  message: exception.message,
  data: null,
  success: false,
});

// 同时你还需要在返回的时候进行描述,当然你可以不必,因为过滤器把这件事都处理了,有的同学说,这么这里讲
// 劈叉了呢,不是拦截器吗怎么就变成过滤器了?原因很简单,因为全局最上层有Filter 错误处理都在fillter 所以要同时
// 处理Filter 和 Interceptor的关系


//如果你代码中有抛Error的地方加上你的特殊标记
throw new UnauthorizedException('您账户已经在另一处登陆,请重新登陆');

上传文件

上传文件无疑是非常常见的功能了,我们来看看在Nest中如何做这件事情,(我们有轮子 不怕!🤪)

  • MulterModule模块

这个是Nestjs提供的一个@nestjs/platform-express 专门用来处理文件,它的使用非常的简单

  MulterModule.registerAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        storage: diskStorage({ // 配置存储
          destination: join(__dirname, '../../../', '/upload'), // 要存储的位置
          filename: (req, file, cb) => { // 文件名处理
            const filename = `${randomUUID()}.${file.mimetype.split('/')[1]}`;
            return cb(null, filename);
          },
        }),
      }),
      inject: [ConfigService],
    }),
  • FileInterceptor UploadedFile 装饰器

仅仅是单上面的内容我们是没办法处理全局的逻辑的,我们需要一个路由来接受参数

// FileInterceptor是从@nestjs/platform-express 导入的一个装饰器,这里是用来处理单个文件📃
@UseInterceptors(FileInterceptor('file'))
  @Post('upload')
  uploadFile(@Body() body: any, @UploadedFile() file: Express.Multer.File) {
    return {
      file: file.filename,
      path: file.path, 
      // 路径请结合前面的main多静态目录来实现 我们只返回文件的相对路径,
      // 为了让外部能够访问 你需要再这里拼上 service 部署的domian地址,
      size: file.size, 
    };
  }
  
// 上述就是一个非常简单的 上传文件了,用form-data格式上传文件上来就好了,下面的例子是关于多文件上传的
@UseInterceptors(FilesInterceptor('files'))
@Post('uploads')
uploadFiles(
@Body() body: any,
@UploadedFiles() files: Array<Express.Multer.File>,
) {
return files.map((item) => ({
  name: item.fieldname,
  path: item.path,
  size: item.size,
}));
}
  • 第三方上传阿里云OSS

有的时候我们不仅仅需要上传到自己的服务器,我们还需要上传到第三方的OSS,在Nest中我们可以集成aliyun的OSS-SDK来做到这个功能,在这里我就不详细的说明 阿里云OSS的操作了 你可以直接去看他们的官方文档 help.aliyun.com/document_de… 我们需要获取你的验证凭据,有了凭据你就能操作它们的OSS了,并且阿里云OSS还提供了Nodejs的SDK 集成,并且对于到底是把 流 存本地还是存内存 其实各有各的处理方案。我这里把他们存本地了不用阿里云的OSS,后期搭配Job定时去清理这些文件

import { HttpService } from '@nestjs/axios';
import {
  Body,
  Controller,
  Get,
  Post,
  Render,
  UploadedFile,
  UploadedFiles,
  UseInterceptors,
  Req,
  Res,
  Param,
} from '@nestjs/common';
import { Request } from 'express';
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
import * as OSS from 'ali-oss';
import multer, { diskStorage } from 'multer';
import path, { join, normalize } from 'path';
import { randomUUID } from 'crypto';

@Controller('files')
export class FilesController {
  oss: OSS;
  constructor(private readonly httpService: HttpService) {
    this.oss = new OSS({
      region: 'oss-cn-beijing',
      accessKeyId: 'yourID',
      accessKeySecret: 'yourKey',
      bucket: 'yourBucket',
    });
  }

  @UseInterceptors(FileInterceptor('file'))
  @Post('upload')
  uploadFile(@Body() body: any, @UploadedFile() file: Express.Multer.File) {
    return {
      file: file.filename,
      path: file.path,
      size: file.size, // 路径请结合前面的main多静态目录来实现
    };
  }

  @UseInterceptors(FilesInterceptor('files'))
  @Post('uploads')
  uploadFiles(
    @Body() body: any,
    @UploadedFiles() files: Array<Express.Multer.File>,
  ) {
    return files.map((item) => ({
      name: item.fieldname,
      path: item.path,
      size: item.size,
    }));
  }

  @Post('upload-oss')
  @UseInterceptors(
    FileInterceptor('file', {
      storage: diskStorage({
        destination: join(__dirname, '../../../', '/upload-oos'),
        filename: (req, file, cb) => {
          const filename = `${randomUUID()}.${file.mimetype.split('/')[1]}`;
          return cb(null, filename);
        },
      }),
    }),
  )
  async oos(@UploadedFile() file: Express.Multer.File) {
    // 上传的时候我们运行你上传到内存中 然后发送给第三方但是这样做不好,
    // 如果存储文件太多或者并非量 你的机器会撑不住,因此我们建议的做法是先存到某
    // 临时目录,然后调用第三方去upload 最后由定时job删除这个up目录就好了
    // 主要还是文件的上传和下载 上传比较简单
    const value = await this.oss.put(file.filename, normalize(file.path));
    return value;
  }

  // 启用oss 下载需要做临时验证
  @Get('upload-oss/:file')
  async getOSSFile(@Param() params: { file: string }) {
    // 上传的时候我们运行你上传到内存中 然后发送给第三方但是这样做不好,
    // 如果存储文件太多或者并非量 你的机器会撑不住,因此我们建议的做法是先存到某
    // 临时目录,然后调用第三方去upload 最后由定时job删除这个up目录就好了
    // 主要还是文件的上传和下载 上传比较简单
    const value = this.oss.signatureUrl(params.file, {
      expires: 3600,
    });

    return {
      url: value,
    };
  }

  • 上面说明了如何存储文件那么如何访问文件呢?

我们需要使用 express.static 来达到这个功能

// main.ts
// 配置文件访问  文件夹为静态目录,以达到可直接访问下面文件的目的
  const rootDir = join(__dirname, '..');
  app.use('/static', express.static(join(rootDir, '/upload')));
  // app.use('/static', express.static(join(rootDir, '/upload'))); // 允许配置多个

请求转发

请求转发相对的简单

// module
---省略部分代码(如果你不知道我在写什么,那么你一定么有好好的阅读我前面的文章)---
    HttpModule.register({
      timeout: 5000,
      maxRedirects: 5,
    }),
    
// httpService是nest内置的一个axios模块 使用前需要去module注入 利用这个我们可以去 “爬”人家的数据了 🐶🐶🐶
 @Get('httpUser')
  async getIpAddress() {
    const value = await this.httpService
      .get('https://api.gmit.vip/Api/UserInfo?format=json')
      .toPromise();
    return { ...value.data };
  }

定时Job

前面我们提过一嘴“要把多余的没有用的上传文件删除掉,在把日志给处理掉”,这些实现都离不开job,我们现在来说说Nest中如何做job

理论知识

  • 首先你需要了解job这个概念和一些常见知识

    定时任务允许你按照指定的日期/时间、一定时间间隔或者一定时间后单次执行来调度。 从字面上也提出好理解就是一个job 一个现定时/不定时的工作项任务,可以重复执行的。我们对它的定时衍生出来了一套规定和语法,在Linux世界中,这经常通过操作系统层面的cron包等执行。

名称含义
* * * * * *每秒
45 * * * * *每分钟第 45 秒
_ 10 _ * * *每小时,从第 10 分钟开始
0 _/30 9-17 _ * *上午 9 点到下午 5 点之间每 30 分钟
0 30 11 * * 1-5周一至周五上午 11:30
  • 我们看看在nest中如何声明job

    在Nestjs中我们使用 @nestjs/schedule 这个库它底层上对node-cron的封装,

// 最简单的job ,(它在你app启动之后,会自动的每45s执行一次)

// 使用前别忘了去注入
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';

@Module({
  imports: [ScheduleModule.forRoot()],
})
export class AppModule {}

// 启动一个服务去使用它
@Injectable()
export class TasksService {


  @Cron('45 * * * * *')
  handleCron() {
    console.log('666')
  }
}

// 它还有很多骚操作比如定义间隔 就像定时器,定义延时的话可以调用这个装饰器 :“@Timeout(5000)”
@Interval(10000)
handleInterval() {
  this.logger.debug('Called every 10 seconds');
}
  • 如何运行它?

上面的我们提到过这个是自动运行的,那么我们有没有可能手动的去启动它,停止它呢?

  // 手动运行
  @Cron('30 * * * * *', {
    name: 'notifications',
  })
  handleTimeout() {
    console.log('66666');
  }
  
 // 在某个 controller/service 中进行手动调度测试
  constructor(
    private schedulerRegistry: SchedulerRegistry

  ) {}

@Get('/job')
async stopJob(@Param() params: { start: boolean }) {
    const job = this.schedulerRegistry.getCronJob('notifications');
    // this.schedulerRegistry 更多详细操作请去看官方文档 https://docs.nestjs.cn/7/techniques?id=%e5%ae%9a%e6%97%b6%e4%bb%bb%e5%8a%a1
    if (params.start) {
      job.start();
      console.log(job.lastDate());
    } else {
      job.stop();
    }
}

实践指南

为了简单起见我们这里只设置了一个job(它用来清理上传到OSS的没有的文件)

// 使用前 你需要去 module中进行注入哈
import { Module } from '@nestjs/common';
import { JobService } from './job.service';

// 这个模块专门处理job 如果其他模块由job的需求,全收敛到这里来处理
@Module({
  imports:[    ScheduleModule.forRoot() ] 
  providers: [],
  controllers: [],
  exports: [JobService],
})
export class JobModule {}


import { Injectable } from '@nestjs/common';
import { Cron, CronExpression, Interval, Timeout } from '@nestjs/schedule';
import * as fs from 'fs';
import { join } from 'path';

// 清除日志目录和本地上传的文件oss临时文件
@Injectable()
export class JobService {
  emptyDir = (fileUrl) => {
    const files = fs.readdirSync(fileUrl); //读取该文件夹
    files.forEach(function (file) {
      const stats = fs.statSync(fileUrl + '/' + file);
      if (stats.isDirectory()) {
        this.emptyDir(fileUrl + '/' + file);
      } else {
        fs.unlinkSync(fileUrl + '/' + file);
      }
    });
  };

  // 每天晚上11点执行一次
  @Cron(CronExpression.EVERY_DAY_AT_11PM)
  handleCron() {
    // 删除OSS文件和日志文件
    const OSSRootDir = join(__dirname, '../../../upload-oos');

    // 日志一般是转存 而不是删除哈,注意 这里只是简单的例子而已
    const accesslogDir = join(__dirname, '../../../logs/access');
    const appOutDir = join(__dirname, '../../../logs/app-out');
    const errorsDir = join(__dirname, '../../../logs/errors');

    this.emptyDir(OSSRootDir);

    this.emptyDir(accesslogDir);
    this.emptyDir(appOutDir);
    this.emptyDir(errorsDir);
  }
}

swagger

swagger 就非常的简单了直接上代码 官方文档在这里 docs.nestjs.cn/7/recipes?i… 虽然文档这额里很简单,我在这里记录一下我的遇到的坑

  • main中加入
  // 构建swagger文档
  const options = new DocumentBuilder()
    .setTitle('Base-Http-example')
    .addBearerAuth()
    .setDescription('一个完善的HttpNodejs服务')
    .setVersion('1.0')
    .addTag('Http')
    .build();
  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup('api', app, document);
  • 去Controller中绑定
@ApiTags('User相关')
@Controller('user')
@UseInterceptors(new HttpReqTransformInterceptor<any>()) // 统一返回体
export class UserController {
    ....省略很多代码,Swagger用法在官方已经描述得非常详细了。各种都有也没什么坑
}
  • 去Dto中写参数说明 也就是写Model
export class UserInfoDTO {
  @ApiProperty({
    description: '名称',
    default: '用户1',
  })
  @IsNotEmpty({ message: '用户名不允许为空' })
  username: string;

  @ApiProperty()
  @IsNotEmpty({ message: '密码不允许为空' })
  password: string;

  @ApiProperty()
  @IsNotEmpty({ message: '更新创建时间必选' })
  @IsNumber()
  update_time: number;

  @ApiProperty()
  @IsNotEmpty({ message: '创建时间必选' })
  create_time: number;

  @ApiProperty()
  @IsNotEmpty({ message: '状态必填' })
  state: number;
}

上面都是最基础的用法,如果还希望有更多的骚气操作请前去官方文档

利用redis做单点登

接下来我们将会说明,如何用。redis做单点登录

  • 首先是安装redis

这个应该对各位来说是比较的简单的哈,可以直接去看 菜鸟教程

  • 我们自己实现一个Module

其实 我们有现成的轮子,但是为零学习 我们还是造了一个
在我们的moduels中实现三个文件 他们分别是cache 的controller module和service (实际上我们只需要service就好了哈哈哈)

@Controller('cache')
export class CacheController {}


@Module({
  imports: [],
  controllers: [CacheController],
  providers: [CacheService],
  exports: [CacheService],
})
export class CacheModule {}

import { Injectable } from '@nestjs/common';
import RedisC, { Redis } from 'ioredis';

@Injectable()
// 目前的版本比较的简单 只是一个设置值清除值,在启动微服务之后,这个地方就不是这样写了
export class CacheService {
  redisClient: Redis;

  // 先做一个最简易的版本,只生产一个 链接实例
  constructor() {
    this.redisClient = new RedisC({
      port: 6379, // Redis port
      host: '192.168.101.10', // Redis host
      family: 4, // 4 (IPv4) or 6 (IPv6)
      password: '',
      db: 0,
    });
  }

  // 编写几个设置redis的便捷方法

  /**
   * @Description: 封装设置redis缓存的方法
   * @param key {String} key值
   * @param value {String} key的值
   * @param seconds {Number} 过期时间
   * @return: Promise<any>
   */
  public async set(key: string, value: any, seconds?: number): Promise<any> {
    value = JSON.stringify(value);
    if (!seconds) {
      await this.redisClient.set(key, value);
    } else {
      await this.redisClient.set(key, value, 'EX', seconds);
    }
  }

  /**
   * @Description: 设置获取redis缓存中的值
   * @param key {String}
   */
  public async get(key: string): Promise<any> {
    const data = await this.redisClient.get(key);
    if (data) return data;
    return null;
  }

  /**
   * @Description: 根据key删除redis缓存数据
   * @param key {String}
   * @return:
   */
  public async del(key: string): Promise<any> {
    return await this.redisClient.del(key);
  }

  /**
   * @Description: 清空redis的缓存
   * @param {type}
   * @return:
   */
  public async flushall(): Promise<any> {
    return await this.redisClient.flushall();
  }
}
  • 有了redis和对他的基础操作之后,我们看看如何实现单点登录

主要的逻辑 是 在签发token的时候把token存到redis中标识key就是用户id,如果下次还有同样的用户id登录就把原来的redis中的key对应的值换成新的,这样先前的那个用户 再次访问的时候发现这个token和之前的不相同,那么就认为它在别的地方登录了,本地就强制下线

image.png

// 在jwt的几个文件中做修改
// /src/moduels/auth/ath.service.ts
  async certificate(user: User) {
    const payload = {
      username: user.username,
      sub: user.id,
    };
    console.log('JWT验证 - Step 3: 处理 jwt 签证');
    try {
      const token = this.jwtService.sign(payload);
      // 把token存储到redis中,如果这个用户下次还登录就把这个值更新了,载validate的时候看看能不能
      // 找到原来的key的值没有就说明更新了就强制要求用户下线 于是这单点登录功能就完成了  ,过期时间和token一致
      await this.CacheService.set(
        `user-token-${user.id}-${user.username}`,
        token,
        60 * 60 * 8,
      );

      return {
        code: 200,
        data: {
          token,
        },
        msg: `登录成功`,
      };
    } catch (error) {
      return {
        code: 600,
        msg: `账号或密码错误`,
      };
    }
  }
  
// jwt逻辑 和local逻辑也要变化一下
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super();
  }

  async validate(username: string, password: string): Promise<any> {
    // 本地local的策略于jwt关系不大,
    console.log('你要调用我哈------------');
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}



@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly CacheService: CacheService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: jwtConstants.secret,
      passReqToCallback: true,
    });
  }

  // JWT验证 - Step 4: 被守卫调用
  async validate(req: Request, payload: any) {
    const originToken = ExtractJwt.fromAuthHeaderAsBearerToken()(req);

    // 只有验证通过之后才会来到这里
    // console.log(`JWT验证 - Step 4: 被守卫调用`);
    const cacheToken = await this.CacheService.get(
      `user-token-${payload.sub}-${payload.username}`,
    );

    //单点登陆验证
    if (cacheToken !== JSON.stringify(originToken)) {
      throw new UnauthorizedException('您账户已经在另一处登陆,请重新登陆');
    }

    return {
      username: payload.username,
    };
  }
}

于是这样就完成了✅了! 比较简单哈,需要注意的就是你设置的key的在redis中的过期时间,还有就是我们实际上可以优化这个代码 把redis链接抽离出来,传递class 而不是在用的时候采取new ,这个各位大佬,可以自己去琢磨实现一下,也👏 欢迎在评论区 留言分享你的实现.

如何做微服务?通信架构如何设计?

使用Nestjs我们将会实现一个非常easy的微服务,通信的话可以使用MQ 也可以直接调用,或者其RPC的方案,但是我没有引入其他复杂的东西,只实现了 直接调用 的方式,在后续的文章中我们会深入的学习其他的调用方式

理论知识

  • 被调用方需要准备什么?

nest中的微服务需要依赖一个库 @nestjs/microservices,使用它我们可以非常方便的创建微服务应用,现在我们启动另一个项目,(很简单,我们啥也不写就是注册一个服务)注意我们使用最简单的TCP来通信,Nest默认情况下,微服务通过 TCP协议 监听消息。

options的值如下

host连接主机名
port连接端口
retryAttempts连接尝试的总数
retryDelay连接重试延迟(ms)
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: {
        host: '192.168.101.2',
        port: 3333,
      },
    },
  );
  app.listen();
}
bootstrap();

// 然后我们去它 controller 写点东西

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
  // 模式2基于 事件 (不一定要求有返回)
  @EventPattern('math:log')
  async handleUserCreated(text: Record<string, unknown>) {
    // business logic
    console.log(text, '基于事件的传输方式');
  }

  // 模式1基于 请求-响应 (要求一来一返)
  @MessagePattern('math:wordcount')
  wordCount(text: string): { [key: string]: number } {
    return this.appService.calculateWordCount(text);
  }
}

  • 什么是模式

模式是微服务之间识别消息的方式,它有下面几种 :

  1. 请求-响应(默认的方式)
  2. 基于事件的,上面你所看到的代码中农已经有了详细的说明
  • 掉用方该如何做才能掉用这个服务呢?
// 同样的在module中注入
@Module({
  imports: [
    ClientsModule.register([
      {
        name: 'NEST_MICRO',
        transport: Transport.TCP,
        options: {
          host: '192.168.101.2',
          port: 3001,
        },
      },
    ]),
    -----省略部分代码-----
    
// 在你将要掉用它的地方 注入它
  constructor(
    private readonly userService: UserService,
    @Inject('NEST_MICRO') private client: ClientProxy,
  ) {}

  // 启用一个微服务
  @Post('/math/wordcount')
  async wordCount(@Body() { text }: { text: string }) {
  // 第一种模式 请求响应
    const value2 = await this.client.send('math:wordcount', text).toPromise();
  // 第二种模式 事件
  await this.client.emit('math:log', text).toPromise();

    return value2;
  }
  • 那么我到底适合哪种模式呢?

一般来说 Kafka 或 NATS 更符合事件模式,(既您只想发布事件而不希望等待的时候⌛️)这种情况下使用事件的方式就很好了(具体的实现 将在后续的文章中揭晓)

实践指南

// 首先在新建一个项目 main中写道 (被掉用方)
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: {
        host: '192.168.101.2',
        port: 3333,
      },
    },
  );
  app.listen();
}
bootstrap();

// 在它的controller中写道 (被掉用方)
import { Controller, Get } from '@nestjs/common';
import { EventPattern, MessagePattern } from '@nestjs/microservices';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
  // 模式2基于 事件 (不一定要求有返回)
  @EventPattern('math:log')
  async handleUserCreated(text: Record<string, unknown>) {
    // business logic
    console.log(text, '基于事件的传输方式');
  }

  // 模式1基于 请求-响应 (要求一来一返)
  @MessagePattern('math:wordcount')
  wordCount(text: string): { [key: string]: number } {
    return this.appService.calculateWordCount(text);
  }
}

// 在我们的掉用方的AppModule全局注入 (掉用方)
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ScheduleModule } from '@nestjs/schedule';
import App_globalConfig from './config/configuration';
import DatabaseConfig from './config/database';
import { AppService } from './app.service';
import { ArticleModule } from './modules/article/article.module';
import { TagModule } from './modules/tag/tag.module';
import { UserModule } from './modules/user/user.module';
import { AuthModule } from './modules/auth/auth.module';
import { FilesModule } from './modules/files/files.module';
import { JobModule } from './modules/job/job.module';
import { CacheModule } from './modules/cache/cache.module';
import { ClientsModule, Transport } from '@nestjs/microservices'; // 注册一个用于对微服务进行数据传输的客户端

@Module({
  imports: [
    ClientsModule.register([  // 同样的你可以使用registrAsync方式读取config配置
      {
        name: 'NEST_MICRO',
        transport: Transport.TCP,
        options: {
          host: '192.168.101.2',
          port: 3001,
        },
      },
    ]),
    ScheduleModule.forRoot(),
    ConfigModule.forRoot({
      isGlobal: true,
      load: [App_globalConfig, DatabaseConfig],
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => {
        return {
          type: 'mysql',
          host: configService.get('database.host'),
          port: Number(DatabaseConfig().port),
          username: DatabaseConfig().username,
          password: DatabaseConfig().password,
          database: DatabaseConfig().database,
          entities: [__dirname + '/**/*.entity{.ts,.js}'], // 扫描本项目中.entity.ts或者.entity.js的文件
          synchronize: true,
        };
      },
      inject: [ConfigService],
    }),
    UserModule,
    TagModule,
    ArticleModule,
    AuthModule,
    FilesModule,
    JobModule,
    CacheModule,
  ],
  providers: [AppService],
})
export class AppModule {}

// 在userModule进行使用
export class UserController {
  constructor(
    private readonly userService: UserService,
    @Inject('NEST_MICRO') private client: ClientProxy,
  ) {}

  // 启用一个微服务
  @Post('/math/wordcount')
  async wordCount(@Body() { text }: { text: string }) {
    const value2 = await this.client.send('math:wordcount', text).toPromise();
    await this.client.emit('math:log', text).toPromise();

    return value2;
  }

Nest到底咋运行的?

如果你拉取我的代码你build之后,然后呢?怎么去部署会运维它呢??🌚🌚 这里我们将会展开讨论到底,嘿嘿这里只是简单的说说,如果大家喜欢看,我后续会完善整个项目

关于build 和运行时

实际上Nest是一个运行时的的框架,它需要和node_module一起start,如果你仅仅是把build的东西就希望它能像前端一样拿出去跑 那就大错特错啦,当然也有不少大神这么干过 ,它们把nest的在build的时候的webpack配置改了,让它去把这些依赖的库全放到build里面去,其实我觉得这样的做法大大的不妥。你可能要处理更多的BUG和不确定,因此我还是推荐大家 不要这么干,老实点把node_module拿出去跑就好了

关于运维

其实我希望描述的是nodejs 在业界的部署方式,拿我司举例(Newegg),我们的部署方式是使用pm2去管理它,同 样的基于pm2的功能我们还完成了自动重启等功能。详情可以去看他们的文档

pm2.fenxianglu.cn/docs/advanc…

k8s和Docker

k8s和Docker的部署比较的简单,我举例子docker的部署吧,在我前面的几篇文章中就有说明,如何使用docker去build一个image,这里不重复的讲了,同样的使用之前文档中的gitlab你完全可以实现自己的工作流。

关于nginx的后端反向代理,

实际上这个在单机中做比较的简单,在docker或者k8s中需要倒腾一下,我这里的方案是在宿主机假设nginx然后映射到容器的prot中去 这里就不过多的介绍了,(nginx使用非常的简单)

参考

NestJS官方文档

TypeOrm官方文档

本项目Github地址