NestJS 使用简介

233 阅读7分钟

NestJS是一个Node服务端开发框架,底层封装的是Express,类似的有阿里的Midway(基于Koa)。这类框架都是提供了标准化的开发规范,以及开箱即用的工具,让开发人员专注业务开发,并输出更告质量的代码。

核心概念

1、Controller

一个Controller就是一个Class,用于处理请求和响应的。根据请求的地址指定处理程序并返回对应结果,也就是充当路由的作用。Controller的配置是利用装饰器,简单高效。

典型用法

import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

请求相关

1)路由配置

装饰器@Controller修饰Class,用来配置Controller类的公共路由地址,地址可以传空,传空就表示访问根目录的响应。 @Get()@Post()@Put()@Delete()@Patch()@Options()@All(), @Head修饰类方法,用来配置对应请求的处理程序,可以传入path,和@Controller的path组成一个完整的路由路径(也就是说路由路径和装饰器修饰的方法名没关系,不过建议和path保持一致)。

注意路由配置支持通配符,如@Controller(hello*)会匹配所有http://127.0.0.1:3000/hello 开头的请求。

如下配置用Get请求 http://127.0.0.1:3000/ 返回 “Get Hello word”,用Post方法请求就返回 “Post Hello word”,用Get请求 http://127.0.0.1:3000/hello 返回 “hello”。

import { Controller, Get, Post } from '@nestjs/common';
import { AppService } from './app.service';

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

  @Get()
  getHello(): string {
    return 'Get Hello word';
  }
  @Get('hello')
  getHello(): string {
    return 'hello';
  }
  @Post()
  postHello(): string {
    return 'Post Hello word';
  }
}

2)Reqeust对象

默认情况下,NestJS封装的请求对象和Express的Reqeust对象相同,并且 NestJS 提供了一些 Request属性的快捷访问。请求相关信息是通过 Controller类的方法参数注入的,需要用相关装饰器修饰,常用修饰器和作用如下: image.png

3) 请求参数

获取请求参数通过读取 Request对象提供的 params、body 属性。NextJS 支持RESTful风格接口设计,因此还支持路由传参,这时通过request.params获取的就是路由参数。

// http://127.0.0.1:3000/hello/111?test=1sdf&123=1 返回 Get Hello word: {"0":"","id":"111"}
@Get(':id')
getHello(@Req() request: Request): string {
    return 'Get Hello word: ' + JSON.stringify(request.params);
}

响应相关

修改响应报文使用NestJS提供的装饰器即可。

  • 修改状态码使用@HttpCode
  • 修改报文头使用@Header
  • 重定向使用 @Redirect

响应内容直接在Controller类的方法中返回响应内容即可,NestJS会自动按最佳返回格式返回内容。

2、Provider

Provider是一个抽象概念,一个Provider具有以下特点:

  • 纯类(类别纯函数),使用 @Injectable 修饰;
  • 可以作为其他模块的依赖,并被IoC容器管理;

由于Provider具有以上特点,默认情况下,一个Provider类会在服务启动时初始化(没有被引用的Provider也会初始化),并且全局只有一个实例,即使这个Provider类被多出引用。

使用Provider的好处是,类的实例化交给了NestJS,消费方只需要声明依赖即可,至于依赖什么时候初始化,什么时候销毁都不用管。

1)Provider声明方式

import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}

2)Provider注入方式

使用constructor声明

import { Controller } from '@nestjs/common';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}
}

等价于
@Controller('cats')
export class CatsController {
  private catsService: CatsService;
  constructor(catsService: CatsService) {
      this.catsService = catsService;
  }
}

3、Modules

使用 @Module() 修饰的类就是一个Module。Module的作用就是用来组织代码的。

NestJS将模块分成了四种类型

  • providers 就是上文的Provider,必须在Module中声明,不然 NestJS 无法做到IoC
  • controllers 就是上文说的Controller
  • imports 用来引入其他模块,引入后其他模块的Provider(需要exports声明)和Controller就可以直接使用了
  • exports 用来导出本模块的Provider,声明后其他模块就可以引入使用了。

异步模块

4、Middleware

默认情况下NestJS的Middleware等价于Express的Middleware

Middleware声明

import { Injectable, NestMiddleware } from '@nestjs/common';

@Injectable()
export class LogMiddlewareMiddleware implements NestMiddleware {
  use(req: any, res: any, next: () => void) {
    console.info('LogMiddlewareMiddleware', req.query);
    next();
  }
}

Middleware使用

export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LogMiddlewareMiddleware)
      .forRoutes({ path: '*', method: RequestMethod.ALL });
  }
}

Middleware特点

  • 可以执行任何代码;
  • 执行时可获得Request和Respond对象,并可以修改;
  • 可以中断请求处理流程,只要不执行next函数即可;
  • 执行了next函数就表示将请求处理交给后续流程,如其他Middleware;
  • 每个请求流程一个Middleware只会执行一次;

执行的顺序是先执行Middleware(多个按注册顺序),再执行Controller逻辑。

4、Exception filters

Exception filter是用来处理服务异常的,当服务出现未捕获异常时,一般要记录日志,返回友好提示或重定向。 异常捕获本身是一个很重要的话题,值得花心思去好好规划服务的异常处理体系,NestJS的Exception filter既是异常处理工具,也是一份异常处理指南。

1)捕获异常

使用@Catch装饰器即可声明一个异常捕获,支持传入多个参数,当不传入参数时表示捕获所有异常,传入参数就表示捕获指定类型的异常。

2)异常处理

implements ExceptionFilter实现catch方法即可,该方法能获取异常对象(HttpException)和执行上下文(ArgumentsHost)。

示例代码

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

@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();

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
      });
  }
}

3)插入异常捕获点

  • 捕获某个方法的异常
@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}
  • 捕获整个Controller的异常
@UseFilters(new HttpExceptionFilter())
export class CatsController {}
  • 捕获全局异常
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();

内置Exception Filter

NestJS内置了一个全局的HttpException Filter,当服务发生未捕获异常时,会默认返回

{
  "statusCode": 500,
  "message": "Internal server error"
}

返回的内容可以自定义,只需要抛出一个HttpException或继承该类的Exception即可,如:

// 自定义异常类
export class ForbiddenException extends HttpException {
  constructor() {
    super('Forbidden', HttpStatus.FORBIDDEN);
  }
}

// Controller中抛出异常
@Get()
async findAll() {
  throw new ForbiddenException();
}

5、Pipes

Pipe是一个实现了PipeTransform接口的Provider。主要用于处理请求入参和校验入参,校验失败时可以抛出异常(然后被Exception Filter处理)。

因为是处理和校验入参的,因此Pipe使用位置肯定是在Controller的方法参数上,并且实在方法执行前执行。

// 以下配置表示:query参数id必须是整数类型或能转成整数类型,不然就会报错
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
  return this.catsService.findOne(id);
}

内置的Pipe

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

自定义Pipe

实现transform方法即可,转换和校验放一个方法,校验不通过抛出异常即可,可以利用第三方校验工具配合使用,如下面使用的joi。

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ObjectSchema } from 'joi';

@Injectable()
export class JoiValidationPipe implements PipeTransform {
  constructor(private schema: ObjectSchema) {}

  transform(value: any, metadata: ArgumentMetadata) {
    const { error } = this.schema.validate(value);
    if (error) {
      throw new BadRequestException('Validation failed');
    }
    return value;
  }
}

6、Guards

Guard是一个实现了CanActivate的Provider。作用如其名,守护对象是Route,也就是作用于Controller,用来判断请求是否能被Controller处理,最典型的就是登录验证了。

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  // 该方法返回true就可以继续往后走,返回false本次请求就返回错误
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}

Guard能做的事情用Middleware也能实现,那为啥要再搞一个Guard呢?

  • Middleware不够灵活,因为不知道next执行后的后续处理程序,对于Middleware注册位置和next的执行时机需要十分注意,不然很容易导致意外的拦截。

7、Interceptor

Interceptor是一个实现了NestInterceptor的Provider。主要用来实现AOP(面向切面编程)。