阅读 544

Nest.js核心概念浅析

背景介绍

NestNestJS) 是一个用于搭建高性能、可伸缩的Node.js服务端应用的框架。它使用渐进式的JavaScript,内置且完全支持Typescript(但开发人员可以使用纯JavaScript进行编程) , 同时结合了OOP(面向对象编程),FP(函数式编程)和FRP(响应式编程)的原理。

Controller

在传统Node.js服务端应用中,controller用于处理route对应的客户端请求。每个controller可以拥有多个route,不同的route会执行不同的action(行为)。在Nest中,我们可以使用内置的decorators来对request, response, controllerrouteaction等元素进行定制化的修饰。从而抽离重复代码,提高开发效率。如下所示:

// 省略依赖代码

@Controller('cats') // 模块名称,用于定义route前缀:/cats
export class CatsController {
  // 修改该route的成功响应时的状态码为204
  @HttpCode(204)
  // 修改该response的响应头参数            
  @Header('Cache-Control', 'none')
  // 修饰该route([POST] /cats)的请求方法:POST,@Get、@Post、@Delete同理
  @Post()
  // @Bady 修饰参数,此处将会传入request.body,@Query()、@Param同理
  create(@Body() createCatDto: CreateCatDto) {
    // 返回响应的数据
    return 'This action adds a new cat';
  }

  @Redirect("http://www.fzzf.top")  // 路由重定向
  @Get(":id")                       // [GET] /cats/:id
  show(@Query("") query: ListAllEntities) {
    // do something
  }
}
复制代码

对于controller的返回值,Nest支持基于async/await返回的promise,也支持基于RxJS返回的Observable Streams,示例如下:

import { of } from 'rxjs';

// controller代码省略

@Put(":id")
async update(@Param() params: UpdateCatDto): Promise<any> {
    return this.catsService.update(params);
}

// 注意不要加async
@Get()
index(@Query() query: IndexCatDto): Observable<any[]> {
    return of(this.catsService.find(query));
}
复制代码

Provider

Nest中,provider被定义为拥有特定功能方法集合的类。通过装饰器@Injectable()修饰后成为可以作为依赖注入到Nest Module的服务。处于相同的作用域的provider之间只要,其中一个provider也可以作为依赖注入到另一个provider中。如下代码所示:

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

@Injectable()
export class CatsService {
  constructor(
    private readonly otherService: OtherService
  )

  async create(values: any): Promise<any> {
    return this.otherService.create(values);
  }
}
复制代码

我们可以声明一个module(参考下一节),将provider注入到controller中。依赖注入的细节由Nest框架内部完成,这里只介绍基本的依赖注入姿势,如下面代码所示:

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

@Module({
  controllers: [CatsController],  // 当前module的controller 
  providers: [CatsService],       // 需要注入的provider
})
export class CatModule {}

// cats.controller
import { CatsService } from './cats/cats.service';

@Controller('cats') 
export class CatsController {
  constructor(
    private readonly catsService: CatsService // 依赖注入
  ) { }

  @Post()
  create(@Body() createCatDto: CreateCatDto) {
    // 返回响应的数据
    return this.catsService.create(createCatDto);
  }
}
复制代码

Module

moduleNest应用的基础组成单位。一个Nest应用至少拥有一个module(root module)。简单来说,在Nest中,n个功能module通过依赖注入组成一个业务module,n个业务 module组成一个Nest应用程序。无论是controller还是provider都需要通过module注册才能完成依赖的注入。下面的代码介绍了基础的module声明姿势。

@Module({
  // 依赖的其他module类
  imports: [],          
  // 业务模块所需的controller类,它将觉得module如何是实例化为业务模块还是功能模块
  controller: [],       
  // 提供依赖的类
  provider: [],       
  // 导出模块中provider,可以被其他模块依赖注入
  exports: [],        
})
export class AppModule {}

复制代码

这里我们可以通过传递exports参数,才完成module之间层级依赖,以及不同module中的provider能够被复用。如下面代码所示:

// request.module.ts
@Module({
  // 注册provider 
  provider: [RequestService], 
  // 导出注册的provider
  exports: [RequestService],        
})
export class RequestModule {}

// api.module.ts
@Module({
  // 导入一个RequestModule,Nest会注册其中export的provider
  imports: [RequestModule],     
  // 注册controller
  controllers: [ApiController], 
})
export class ApiModule {}

// api.controller.ts
@Controller('api') 
export class ApiController {
  constructor(
    private readonly requestService: RequestService  // 依赖注入
  ) { }
}
复制代码

除此之外,我们还可以通过装饰器@Globalmodule A修饰为一个全局模块,这样任意module不需要import就可以注入module Aexport的依赖。同时我们也可以通过特定参数实现module动态导入

Middleware

Nest是一个上层框架,底层依赖于express框架(或hapi),所以Nestmiddleware等同于express中的middleware,我们可以使用它来访问requestresponse,以及对处理程序做一些前置或后置操作。它的声明方式如下:

  • 声明
// 方式1
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';

@Injectable()
export class CatchMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: Function) {
    console.log('catch...');
    next();
  }
}

// 方式2
export function catchError(req, res, next) {
  console.log(`catch...`);
  next();
};
复制代码
  • 使用
// 方式1
@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(CatchMiddleware)
      .forRoutes(CatsController);
  }
}

// 方式2
const app = await NestFactory.create(AppModule);
app.use(catchError);
await app.listen(3000);
复制代码

Exception filters

Nest内置了处理程序未处理的异常捕获层(exception-filter)以及友好的异常处理对象。我们通过声明exception来对catch全局或局部的异常抛出,如全局的error。如下面代码所示:

http-exception.filter.ts

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

// @Catch()参数为空时,catch所有类型的异常
@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,
      });
  }
}

// 使用
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}
复制代码

同时我们还可以继承Nest内置的exception对象来规范化系统异常的格式,如下代码所示:

export class ForbiddenException extends HttpException {
  constructor() {
    super('Forbidden', HttpStatus.FORBIDDEN);
  }
}
// 错误打印
{
  "status": 403,
  "error": "This is a custom message"
}
复制代码

Pipes

PipeNest中用于对controller中处理程序的request做前置处理。

  • 格式化请求输入,如req.query、req.param、req.body等;
  • 校验请求输入,若正确则按原样传递,反正则抛出异常exception; 这里声明方式可以参考官方文档

Guards

Nest中,Guard被设计拥有单一指责的前置处理程序,用于通知route对应处理函数是否需要执行。官方文档中推荐使用Guard来实现 authorization相关的功能,如权限校验、角色校验等,来替代传统express应用程序中使用middleware实现的思路。

官方认为middleware本质上是很“愚蠢”的,因为它无法知道在调用next之后将执行哪一个处理函数。而Guard可以通过ExecutionContext获取当前request的执行上下文,清楚的知道哪一个handler将要执行,因此可以保证开发者在正确的requestresponse周期添加处理逻辑。

它的声明方式如下:

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

// 注意函数声明
@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}
复制代码

同时guard也支持全局、单个controller或单个handle method进行前置处理。详情可以参考官方文档

Interceptors

Nest中,Interceptor的设计灵感来自于[AOP](https://en.wikipedia.org/wiki/Aspect-oriented_programming),用于给某个处理函数模块(如controller)添加前置/后置操作。实现功能如:输入/输出的额外操作、exception的过滤、在特定条件下重载当前处理函数的逻辑等。它的声明方式如下:

import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class HttpInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
   // context当前执行上下文
   // next下一个将要执行的处理函数
    return next.handle().pipe(
      map((value: any) => ({
        code: 0,
        data: value,
      }))
    );
  }
}

// 使用
UseInterceptors(HttpInterceptor)
export class CatsController {}
复制代码

Request Lifecycle

以上介绍的各种Nest组件被注册之后由Nest框架管理,它们在一个请求的某个声明周期阶段被执行。具体执行顺序如下:

  1. 请求输入
  2. 全局middleware
  3. 根模块(root modulemiddleware
  4. 全局Guards
  5. Controller Guards
  6. route对应处理函数上使用@UseGuards()注册的Guard
  7. 全局interceptors(controller前置处理)
  8. controllerinterceptors(controller前置处理)
  9. route对应处理函数上使用@UseInterceptors()注册的interceptors(controller前置处理)
  10. 全局pipes
  11. controllerpipes
  12. route对应处理函数上使用@UsePipes()注册的Pipes
  13. route对应处理函数参数注册的Pipes (如:@Body(new ValidationPipe())
  14. route对应处理函数
  15. route对应处理函数依赖的Serivce
  16. route对应处理函数上使用@UseInterceptors()注册的interceptors(controller后置处理)
  17. controllerinterceptors(controller后置处理)
  18. 全局interceptors(controller后置处理)
  19. route对应处理函数注册的Exception filters
  20. controller注册的Exception filters
  21. 全局注册的Exception filters
  22. 响应输出

参考资料