从请求流程看Nest分层
内行人都知道,NestJS俗称SpringJS
,又或者说面向angularJS编程,涉及到了很多概念如IOC控制反转、DI依赖注入、面向AOP编程等等,设计者的确参考了其他框架的设计思想,对Node企业级应用进行了标准化架构,解决了Node生态中架构缺失问题。
除了上述提到的概念之外,我们多少了解关于Nest部分模块,如常见的控制器controller
、服务service
、过滤器filter
、守卫guard
、拦截器interceptor
、管道Pipe
和中间件middleware
,知道这些模块的基本使用,或许也产生了另个几个问题:
- 在实际应用中这些模块起到什么作用呢?
- 在一个实际的请求过来的时候,他们的执行顺序又是怎么样?
- 我应该在开发中怎么利用这些模块去搭建一套标准的项目架构?
这节就先从全局的视角来梳理一下Nest分层。
Nest分层
当一个请求打过来的时候,通过指定路由进入对应模块的Controller
,这里维护着一些列的服务端函数,通过指定的method
进入对应的Service
层,后续进行操作db
或者返回数据给客户端,完成大概的请求流程。
Nest
在这个基础上,通过AOP
的思想引入了过滤器filter
、守卫guard
、拦截器interceptor
、管道Pipe
和中间件middleware
等,他们分别承担着不同的责任来解决不同的实际问题,形成了以下分层:
中间件(middleware)
请求最先经过的是中间件,Nest
默认使用的是express
中间件,而express
拥有比较完善生态,在Nest
中依然可以使用这些中间件来应用实际问题。 比如中间件可以用来解决以下问题:
- 执行任何代码。
- 对请求和响应对象进行更改。
- 结束请求-响应周期。
- 调用堆栈中的下一个中间件函数。
- 如果当前的中间件函数没有结束请求-响应周期, 它必须调用 next() 将控制传递给下一个中间件函数。否则, 请求将被挂起。
比如使用Logger
、cors
中间件解决日志和请求跨域问题,这也是为什么放在前面的原因。
使用Logger
解决统计日志问题,本文主要在于抛砖引玉,关于如何使用Log4js
,因篇幅问题不详细展开,自行查阅~
typescript
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
next();
// 组装日志信息
let logStr = `[method: ${req.method}; path: ${req.url};
params: ${req.params}; query: ${req.query} status: ${res.statusCode};]`
// 调用Log4js来统计日志信息
// Logger.access(logStr);
}
}
守卫(guard)
守卫,顾名思义就像古代的帝皇身边的贴身守卫,他们要保证没有授权的人是不能够接近目标。在Nest
中,守卫的职责也有单一的责任,就是用来处理授权问题(如权限、角色控制),决定是否可以请求某个路由。
守卫在每个中间件之后执行,但在任何拦截器或管道之前执行。
由于中间件无法知道调用next
之后将会由哪个程序进行处理,而守卫可以访问ExecutionContext
从而知道接下来要执行哪些操作。
typescript
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class PersonGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
console.log('进入守卫');
return true;
}
}
拦截器(interceptor)
从上图中可以看到,controller
方法前后
都可以能够被interceptor
拦截到,所以可以在处理函数之前或之后
做以下处理操作:
- 在函数执行之前/之后绑定额外的逻辑
- 转换从函数返回的结果
- 转换从函数抛出的异常
- 扩展基本函数行为
- 根据所选条件完全重写函数 (例如, 缓存目的)
下面演示操作函数返回的结果:
typescript
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import { Observable, map, timeout } from "rxjs";
@Injectable()
export class TimeoutInterceptor implements NestInterceptor{
intercept(
context: ExecutionContext,
next: CallHandler
): Observable<any>{
console.log('进入拦截器');
return next.handle().pipe(
map((value) => {
console.log('value: ' + JSON.stringify(value));
})
);
}
}
管道(Pipe)
管道的主要职责在于两种:
- 转换:管道将输入数据转换为所需的数据输出(例如,将字符串转换为整数)
- 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常
管道验证的参数会由controller
服务处理函数调用的时候进行处理,Nest
会在调用路由方法之前插入管道拦截方法的参数进行验证或转换,处理好之后再进行调用原方法。
内置管道
Nest
自带九个开箱即用的管道,即
- ValidationPipe
- ParseIntPipe
- ParseFloatPipe
- ParseBoolPipe
- ParseArrayPipe
- ParseUUIDPipe
- ParseEnumPipe
- DefaultValuePipe
- ParseFilePipe
当然,我们可以自定义管道对参数进行验证和转换,必要的时候抛出指定的异常:
typescript
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from "@nestjs/common";
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
console.log('进入管道', value, metadata);
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed');
}
return val;
}
}
关于管道的验证器有很多种,上面演示的是在单独的路由方法中验证指定的参数,当需要验证的参数有多个的时候,显然会变的臃肿,这时候采用DTO
方式进行验证,并且在全局注册ValidationPipe
对所有的绑定DTO
的方法进行统一验证。
过滤器(filter)
从上图可以看到,异常过滤器是包含着几个模块,表示其他的拦截器或者守卫中抛出异常,异常过滤器都将会捕获到。
Exception filters
异常过滤器可以捕获在后端接受处理任何阶段所跑出的异常,捕获到异常后,然后返回处理过的异常结果给客户端(比如返回错误码,错误提示信息等等)。
我们需要再请求异常的时候统一返回相同格式的异常,而不是统一的500服务端错误,基于这个需求可以自定义HttpException
过滤器:
typescript
import { ArgumentsHost, Catch, ExceptionFilter, 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();
const message = exception.message;
let resMessage: string | Record<string, any> = exception.getResponse();
console.log('进入异常过滤器');
if (typeof resMessage === 'object') {
resMessage = resMessage.message
}
response.status(status).json({
message: resMessage || message,
success: false,
path: request.url,
status
});
}
}
在Nest
中,为了减少开发者手动定义过滤器,内置了多种请求异常过滤器,开发者可以自行抛出:
- BadRequestException
- UnauthorizedException
- NotFoundException
- ForbiddenException
- NotAcceptableException
- RequestTimeoutException
- ConflictException
- GoneException
- PayloadTooLargeException
- UnsupportedMediaTypeException
- UnprocessableException
- InternalServerErrorException
- NotImplementedException
- BadGatewayException
- ServiceUnavailableException
- GatewayTimeoutException
其他异常
除了HttpExceptionFilter
之外,其他所有的异常都应该被捕获,那么可自定义AllExceptionsFilter
,并且在全局中进行绑定。
总结
梳理了Node
分层之后,对整体的请求流程有了清晰的路线,在发送请求到响应请求,经过了层层校验和转换,最终回到客户端。 接下来的就是去深入每一个环节,在实际企业应用中完善应用场景。