likes
comments
collection
share

NestJS从入门到跑路-01-基础

作者站长头像
站长
· 阅读数 4

Nest

废话: 犹豫了很久才开始动笔,使用Nest以后发现很香,比koa,egg。但由于ts使用较少,很多玩意不知其所以然。所以开始动手记录

也不知道有没有人看,如果对你有帮助请留下你的评论,我也好有动力更新实战高阶~

没错就是这么任性- -。

为什么使用Nest

在熟悉使用了express, koa之后,每次新增项目都需要对之前引入的中间件进行筛选重新引入,太过灵活在企业级应用开发中的弊端也开始显现,而js的若类型特点的弊端被放大,需要写很多代码进行规避。而中间件的选择使用也需要花费大量时间去进行集成封装,费时费力

而egg,nest这种高集成的企业级框架解决了上述问题,二者各有优势,但nest在高集成的同事还是保留了express所有的功能,并且对ts的支持更好。故选择。

安装

npm i -g @nestjs/cli

nest new project-name

2022/04/01 zhangyue

架构

[TOC]

Koa

之前使用koa的时候,我们使用mvc模式,将项目划分为model,controller,service,router来进行基础功能,其他均创建在middleway,config中。

功能实现,但在项目体量上来以后维护还是麻烦。需要写大量的重复的代码,体验很差。

src
├── README.md
├── app.js
├── config
│   ├── database.config.js
│   └── secret.json
├── controller
│   ├── code.js
├── error.md
├── koa.log
├── middlewares
│   ├── error.js
│   └── token.js
├── models
│   ├── code.js
│   ├── index.js
├── package.json
├── routers
│   ├── code.js
├── service
│   ├── code.js
├── static
│   ├── images
│   │   ├── 1.jpg
│   │   ├── 2.jpg
│   │   ├── 3.jpg
│   │   └── 4.jpg
│   └── upload
└── utils
    ├── constants.js
    ├── customError.js
    ├── errorMsg.js
    └── response.js

架构

这也是最纠结,文档也没有说如何去进行设计。如果还是按照koa那样设计,nest的优势也就看不太出来

先看看成品,半吊子后端开发研究的。慢慢再改进吧

src
├── README.md
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
├── filters
│   ├── any-exception.filter.spec.ts
│   ├── any-exception.filter.ts
│   ├── http-exception.filter.spec.ts
│   └── http-exception.filter.ts
├── interceptor
│   ├── auth
│   │   ├── auth.module.ts
│   │   ├── auth.service.spec.ts
│   │   ├── auth.service.ts
│   │   ├── constants.ts
│   │   └── jwt.strategy.ts
│   └── transform.interceptor.ts
├── main.ts
├── middleware
│   ├── logger.middleware.spec.ts
│   └── logger.middleware.ts
├── pipe
│   ├── validation.pipe.spec.ts
│   └── validation.pipe.ts
├── user
│   ├── dto
│   │   └── user.dto.ts
│   ├── entities
│   │   ├── user.entity.ts
│   │   └── user_info.entity.ts
│   ├── user.controller.spec.ts
│   ├── user.controller.ts
│   ├── user.module.ts
│   ├── user.service.spec.ts
│   └── user.service.ts
└── utils
    ├── cryptogram.ts
    └── log4js.ts

基础 - Providers

Nest较我们之前了解的node,还是有一些新概念的加入,这对我们学习至关重要。

Provider 只是一个用 @Injectable() 装饰器注释的类。

@injectable 一般用在Angular的Service中,他的意思是该Service实例可以注入到其他的service、component或者其他实例里面。换句话说,就是其他的实例要依赖他。

user/user.service.ts中,我们定义了UserService类, 并使用@Injectable()进行装饰

Service

@Injectable()
export class UserService {
  constructor() {}
}

此时,UserService 就可以供我们进行使用。比如,在auth.service中的登陆接口,我们需要验证用户信息,就要用到UserService。

依赖注入

熟悉angular的并不陌生,在nest中,借助typescript功能,管理依赖非常容易,因为他仅按类型进行解析。

// auth.service.ts

import { Injectable } from '@nestjs/common';
import { UserService } from '../../user/user.service';

@Injectable()
export class AuthService {
  constructor(
    private readonly usersService: UserService,
    private readonly jwtService: JwtService,
  ) {}
}

通过以上操作,我们将其注入auth.service, 我们就可以在autvicede.servic中使用user.service的方法。

基础 - 模块

模块是具有 @Module() 装饰器的类。 @Module() 装饰器提供了元数据,Nest 用它来组织应用程序结构。

每个 Nest 应用程序至少有一个模块,即根模块(app.module.ts)。

@module() 装饰器接受一个描述模块属性的对象:

  • providers - 由 Nest 注入器实例化的提供者,并且可以至少在整个模块中共享

  • controllers - 必须创建的一组控制器

  • imports - 导入模块的列表,这些模块导出了此模块中所需提供者

  • exports - 由本模块提供并应在其他模块中可用的提供者的子集。

功能模块

还是以user,userInfo和auth的例子来说

关系描述:

  1. user想使用userInfo
  2. auth想使用user
// user.model.ts

import { User } from './entities/user.entity';
import { UserInfo } from './entities/user_info.entity';


@Module({
  imports: [TypeOrmModule.forFeature([User, UserInfo])],
  // controllers: [UserController],
  providers: [UserService],
  exports: [UserService],
})
export class UserModule {}


此模块使用 forFeature() 方法定义在当前范围中注册哪些存储库

这样,我们就可以使用 @InjectRepository()装饰器将 UsersRepository 注入到 UsersService 中

//auth.module.ts


import { UserModule } from '../../user/user.module';


@Module({
  imports: [UserModule,],
  providers: [AuthService, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

⚠️ 细心的都发现了,controllers被注释和删除了。这里写会有个报错。。。。放到app.module中统一注册了

全局模块

如果你不得不在任何地方导入相同的模块,那可能很烦人。提供者是在全局范围内注册的。一旦定义,他们到处可用。

import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Global()
@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})
export class CatsModule {}

Middleway

nest中间件同koa一样,都是基于express

差异不大,都是在路由处理之前调用的函数。中间件函数可以访问请求和响应的对象,以及应用程序请求相应周期重的next()中间件函数。

日志中间件

import { Request, Response } from 'express';
import { Logger } from '../utils/log4js';

export function LoggerMiddleware(req: Request, res: Response, next: () => any) {
  const code = res.statusCode; // 响应状态码
  next();
  // 组装日志信息
  const logFormat = ` >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    Request original url: ${req.originalUrl}
    Method: ${req.method}
    IP: ${req.ip}
    Status code: ${code}
    Parmas: ${JSON.stringify(req.params)}
    Query: ${JSON.stringify(req.query)}
    Body: ${JSON.stringify(
      req.body,
    )} \n  >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
  `;
  // 根据状态码,进行日志类型区分
  if (code >= 500) {
    Logger.error(logFormat);
  } else if (code >= 400) {
    Logger.warn(logFormat);
  } else {
    Logger.access(logFormat);
    Logger.log(logFormat);
  }
}



应用中间件

// main.ts

const app = await NestFactory.create(AppModule);

app.use(LoggerMiddleware);

await app.listen(7001);

Filter

内置的异常层负责处理整个应用程序中的所有抛出的异常。当捕获到未处理的异常时,最终用户将收到友好的响应。

之前我们处理系统异常都是通过中间件来进行捕获,但是Nest给我们提供了专门的异常过滤模块。

一般情况下,我们将异常分为2类,代码异常(自定义异常),Http异常。

在@nestjs/common中内置了很多可用异常,开袋即食

自定义异常

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { Logger } from '../utils/log4js';


@Catch()
export class AnyExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const logFormat = ` <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    Request original url: ${request.originalUrl}
    Method: ${request.method}
    IP: ${request.ip}
    Status code: ${status}
    Response: ${exception} \n  <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    `;
    Logger.error(logFormat);
    response.status(status).json({
      code: status,
      success: false,
      msg: `Service Error: ${exception}`,
    });
  }
}

Http异常

HttpException 构造函数有两个必要的参数来决定响应:

  • response 参数定义 JSON 响应体。它可以是 string 或 object,如下所述。
  • status参数定义HTTP状态代码。

默认情况下,JSON 响应主体包含两个属性:

  • statusCode:默认为 status 参数中提供的 HTTP 状态代码
  • message:基于状态的 HTTP 错误的简短描述
@Get()
async findAll() {
  throw new HttpException({
    status: HttpStatus.FORBIDDEN,
    error: 'This is a custom message',
  }, HttpStatus.FORBIDDEN);
}


进阶版


import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { Logger } from '../utils/log4js';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    const logFormat = ` <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    Request original url: ${request.originalUrl}
    Method: ${request.method}
    IP: ${request.ip}
    Status code: ${status}
    Response: ${exception.toString()} \n  <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    `;
    Logger.error(logFormat);
    response.status(status).json({
      code: status,
      error: exception.message,
      success: false,
      msg: `${status >= 500 ? 'Service Error' : 'Client Error'}`,
    });
  }
}

使用

1 - 通过封装模块以中间件的方式在main.ts中全局使用

  app.useGlobalFilters(new HttpExceptionFilter());
  app.useGlobalFilters(new AnyExceptionFilter());

2 - 与接口绑定


@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

一般情况下, 建议还是封装使用吧。你不嫌累随意。

一次封装终身使用

其他说明

  1. @Catch

为了捕获每一个未处理的异常(不管异常类型如何),将 @Catch() 装饰器的参数列表设为空,例如 @Catch()。

  1. ArgumentsHost

ArgumentsHost 是用来来获取所需的 Request 和 Response 对象

  1. HttpStatus

它是从 @nestjs/common 包导入的辅助枚举器。

  1. HttpException

内置类,它从 @nestjs/common 包中导入。对于典型的基于HTTP REST/GraphQL API的应用程序,最佳实践是在发生某些错误情况时发送标准HTTP响应对象。

  1. ExceptionFilter

所有异常过滤器都应该实现通用的 ExceptionFilter<T> 接口。它需要你使用有效签名提供 catch(exception: T, host: ArgumentsHost)方法。T 表示异常的类型。

Pipe

管道是具有 @Injectable() 装饰器的类。

管道应实现 PipeTransform 接口。

管道有两个类型:

  • 转换:管道将输入数据转换为所需的数据输出
  • 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常;

“看到这里沉思了好久,过滤器,拦截器,管道,任何一个都可以干其他的事,为什么要有这些。”

根据他的描述,管道事用来验证request参数的东西。

Nest 贴心的内置了八个常用管道

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • DefaultValuePipe
  • ParseEnumPipe
  • ParseFloatPipe

“8个看着花里胡哨,就一个ValidationPipe有点用”

管道

ValidationPipe 需要同时安装 class-validator 和 class-transformer 包

每个管道必须提供transform()方法,有两个参数

  • value 当前处理参数
  • metadata 元数据

metadata

什么是元数据?

元数据对象包含一些属性

  • type

告诉我们该属性是一个 body @Body(),query @Query(),param @Param() 还是自定义参数

  • metatype

属性的元类型,例如 String。 如果在函数签名中省略类型声明,或者使用原生 JavaScript,则为 undefined

  • data

传递给装饰器的字符串,例如 @Body('string')。 如果您将括号留空,则为 undefined。

export interface ArgumentMetadata {
  type: 'body' | 'query' | 'param' | 'custom';
  metatype?: Type<unknown>;
  data?: string;
}

TypeScript接口在编译期间消失,所以如果你使用接口而不是类,那么 metatype 的值将是一个 Object。

PipeTransform

PipeTransform<T, R> 是一个通用接口,其中 T 表示 value 的类型,R 表示 transform() 方法的返回类型。

BadRequestException

内置异常过滤器

类验证器

Nest 与 class-validator 配合得很好。这个优秀的库允许您使用基于装饰器的验证。装饰器的功能非常强大,尤其是与 Nest 的 Pipe 功能相结合使用时,因为我们可以通过访问 metatype 信息做很多事情,在开始之前需要安装一些依赖。

npm i --save class-validator class-transformer

Demo

import {
  ArgumentMetadata,
  Injectable,
  PipeTransform,
  BadRequestException,
} from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
import { Logger } from '../utils/log4js';


@Injectable()
export class ValidationPipe implements PipeTransform {

  async transform(value: any, { metatype }: ArgumentMetadata) {
    console.log(`value:`, value, 'metatype: ', metatype);
    if (!metatype || !this.toValidate(metatype)) {
      // 如果没有传入验证规则,则不验证,直接返回数据
      return value;
    }
    // 将对象转换为 Class 来验证
    const object = plainToClass(metatype, value);
    const errors = await validate(object);
    if (errors.length > 0) {
      const msg = Object.values(errors[0].constraints)[0]; // 只需要取第一个错误信息并返回即可
      Logger.error(`Validation failed: ${msg}`);
      throw new BadRequestException(`Validation failed: ${msg}`);
    }
    return value;
  }
  private toValidate(metadata: any): boolean {
    const types: any[] = [String, Boolean, Number, Array, Object];
    return !types.includes(metadata);
  }
}


  • transform() 函数是 异步 的, Nest 支持同步和异步管道。这样做的原因是因为有些 class-validator 的验证是可以异步的(Promise)

  • toValidate() 方法。当验证类型不是 JavaScript 的数据类型时,跳过验证。

  • 使用 class-transformer 的 plainToClass() 方法来转换 JavaScript 的参数为可验证的类型对象。一个请求中的 body 数据是不包含类型信息的,Class-validator 需要使用前面定义过的 DTO,就需要做一个类型转换。

  • 如前所述,这就是一个验证管道,它要么返回值不变,要么抛出异常。

使用

基于参数

@Post()
async create(@Body(new ValidationPipe()) createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

基于方法

@Post()
@UsePipes(new ValidationPipe())
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

全局使用

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

Dto

数据传输对象(DTO)(Data Transfer Object),是一种设计模式之间传输数据的软件应用系统。数据传输对象与数据交互对象或数据访问对象之间的差异是一个以不具有任何行为除了存储和检索的数据(访问和存取器)

在Pipe中,我们使用class-transformer 的 plainToClass() 方法来转换 JavaScript 的参数为可验证的类型对象,但是一个请求中的 body 数据是不包含类型信息的。

所以需要DTO来进行配合

使用

export class RegisterInfoDTO {
  @IsNotEmpty({ message: '用户名不能为空' })
  readonly name: string | number;
  @IsNotEmpty({ message: '密码不能为空' })
  readonly password: string | number;
}

nest 建议使用class而非interface

  @UsePipes(new ValidationPipe())
  @Post('register')
  async register(@Body() req: RegisterInfoDTO): Promise<any> {
    return await this.user.register(req);
  }

其他

class-validator常用示例

  • ---------- 类型判断 -------------
  • IsInt : @IsInt()
  • Length : @Length(10, 20)
  • IsEmail : @IsEmail()
  • IsFQDN : @IsFQDN() - 完全合格域名
  • IsDate : @IsDate()
  • IsNumber : @IsNumber()
  • IsString : @IsString()
  • IsBoolean : @IsBoolean()
  • IsArray : @IsArray()
  • ---------- 常用类型 -------------
  • Equals : @Equals(comparison: any) - 检查值是否等于(“===”)比较
  • Contains : @Contains(seed: string) - 检查字符串是否包含种子
  • Min : @Min(0)
  • Max : @Min(0)
  • MinDate : @MinDate(date: Date)
  • MaxDate : @MaxDate(date: Date)
  • IsEmpty : @IsEmpty
  • IsNotEmpty : @IsNotEmpty
  • ISNotIn : @ISNotIn(values: any[]) - 检查value是否不在一个不允许的值数组中
  • ISIn : @ISIn(values: any[])
  • ---------- 这玩意太多 写是写不完 边用边查吧 -------------
  • ---------- github.com/typestack/c… ------------- */

Guard

守卫有一个单独的责任。它们根据运行时出现的某些条件(例如权限,角色,访问控制列表等)来确定给定的请求是否由路由处理程序处理。 这通常称为授权。

在传统的 Express 应用程序中,通常由中间件处理授权。中间件是身份验证的良好选择。到目前为止,访问限制逻辑大多在中间件内。这样很好,因为诸如 token 验证或将 request 对象附加属性与特定路由没有强关联。

守卫在每个中间件之后执行,但在任何拦截器或管道之前执行。

守卫也是使用@Injectable()装饰的类,守卫应该实现 CanActivate 接口。

授权守卫

AuthGuide

角色守卫

RolesGuide

绑定守卫

@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}