什么是 AOP
Springboot 中就存在 AOP 切面编程。在 Nest 中也同样提供了该能力。
通常,一个请求过来,可能会经过 Controller(控制器)、Service(服务)、DataBase(数据库访问)的逻辑。
在这个流程中,若想要添加一些通用的逻辑,比如 日志记录、权限控制、异常处理等作为一个通用的逻辑。
AOP 的目的就是将那些与业务无关,却需要被业务所用的逻辑单独封装,以减少重复代码,减低模块之间耦合度,以方便项目的迭代与维护。
Nest 中的 AOP 有五种:Middleware
中间件、Guard
守卫、Pipe
管道、Interceptor
拦截器、ExceptionFilter
异常过滤器。
Middleware
全局中间件
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(function (req: Request, res: Response, next: NextFunction) {
console.log('before-----', req.url);
next();
console.log('after-----');
});
await app.listen(3000);
}
当访问 http://localhost:3000/person 时,则会触发打印,同时,访问其它请求地址同样会触发该中间件逻辑,这就是全局中间件,可以在多个 handler 之间复用中间件的逻辑。
路由中间件
Nest 中除了全局中间件,还有路由中间件。
创建一个路由中间件
# --no-spec 不生成测试文件,--flat 平铺,不生成目录
nest g middleware log --no-spec --flat
在创建出的文件中打印以下前后
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
@Injectable()
export class LogMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: () => void) {
console.log('before2', req.url);
next();
console.log('after2');
}
}
然后在 AppModule 中启用该中间件
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LogMiddleware).forRoutes('person*');
}
}
其中 forRoutes
中就是指定该中间件在匹配到哪些路由时可以生效
路由中间件可以明确或者泛指定哪些路由可以使用该中间件,其余是无法触发该中间件的
全局中间件则是所有的路由都会触发该中间件,没有任何约束
Guard
Guard 是路由守卫的意思,可以用于在调用某个 Controller 之前判断权限,返回 true 或者 false 来决定是否放行。
创建一个 Guard
nest g guard login --no-spec --flat
Guard 需要实现 CanActivate 接口,实现 canActivate 方法,可以从 context 拿到请求的信息,然后做一些权限验证等处理之后返回 true 或者 false。
简单使用
对生成的 guard 文件内容返回 false,
@Injectable()
export class LoginGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
// 其中 context 是该此请求调用的执行上下文
// 比如 context.getClass(), context.getHandler() 得到的打印就是 [class PersonController] [Function: findAll]
console.log('guard log');
return false;
}
}
其中 context
参数是执行上下文,可以用来获取此次调用的上下文信息,比如context.getClass()
context.getHandler()
,访问 /person
,得到的打印就是如下图所示内容,其中 context.getClass()
获取当前路由的类,context.getHandler()
可以获取到路由将要执行的方法
在某个请求(此处为 PerosnController 的 /person 路由
)的 Controller 上添加 @UseGuards(LoginGuard)
然后访问 http://localhost:3000/person ,请求报错,并返回无权限的提示信息
就像这样,Controller 本身不需要做啥修改,只需要加一个装饰器,就加上了权限判,这就是 AOP 架构的好处。
而且,就像 Middleware 支持全局级别和路由级别一样,Guard 也可以全局启用
全局守卫
guard 的全局守卫有两种添加方式,可以在 main.ts
的 app 中添加,也可以在 AppModule 中添加,但是两者之间在使用上是有一些不同。
main.ts 中添加全局守卫
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 添加全局守卫
app.useGlobalGuards(new LoginGuard());
await app.listen(3000);
}
此时由于 LoginGuard
中直接写死返回 false,所以访问任何一个路由都是 403 无权
AppModule 中添加全局守卫
在 AppModule 中添加时,需要注意,全局守卫是在 providers
中进行关联,且其 proovide
提供的 token,必须是由 @nestjs/core
导出的 APP_GUARD
import { APP_GUARD } from '@nestjs/core';
import { LoginGuard } from './login.guard';
@Module({
imports: [PersonModule],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_GUARD, // 注入的 token 必须是由 @nestjs/core 导出的
useClass: LoginGuard,
},
],
})
两种注册方式的区别
在 main.ts
中,由 app 注册的,是手动 new 的实例,是不在 IOC 容器中的,无法执行注入操作
@Injectable()
export class LoginGuard implements CanActivate {
// 注入一个服务,调用其方法
@Inject(PersonService)
private readonly personService: PersonService;
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
console.log('guard log', this.personService.findAll());
return false;
}
}
在使用 app 注册全局守卫时,访问任何一个路由都报错,无法找到注入的服务模块。
而如果使用 AppModule 注册全局守卫,由于 Guard 是在 IOC 容器中,所以可以正常执行注入操作
当模块需要注入其它 provider 时,则需要在 AppModule 中进行注册声明
Interceptor
Interceptor
(拦截器) 可以处理请求处理过程中的请求和响应,例如身份验证、日志记录、数据转换等,其作用和 Middleware
较为相似,但却有明显的不同之处。可参考文档 Nest 中文网-拦截器
新建一个 interceptor 模板:
nest g interceptor test --no-spec --flat
// test.interceptor.ts
@Injectable()
export class TestInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// 这里的 context 和 Guard 的 context 差不多
console.log(context.getClass(), context.getHandler());
return next.handle();
}
}
在 main.ts
中进行全局注册
import { TestInterceptor } from './test.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new TestInterceptor());
await app.listen(3000);
}
拦截器的简单应用
interceptor 与 middleware 其中不同的一点就是,interceptor 可以获取到数据信息,并对其进行操作,rjsx
中提供了多种功能,例如 map
tap
等。
同时 interceptor 内部可以获取到 context
,和 guard
一样,而 middleware 是不行的。
- 对返回数据进行格式化包装
export interface Response<T = any> {
code: number;
data: T;
}
@Injectable()
export class TestInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<Response> {
// 返回的结果都会被格式化为 {code:xxx, data:...} 的格式
return next.handle().pipe(map((data) => ({ code: 200, data })));
}
}
拦截器的全局注册和局部注册
interceptor
的注册方式和 guard
类似
- 全局注册(方式一:app 注册)
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new TestInterceptor());
await app.listen(3000);
}
- 全局注册(方式二:AppModule 注册)
@Module({
imports: [PersonModule],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_INTERCEPTOR, // 该常量由 @nestjs/core 导出
useClass: TestInterceptor,
},
],
})
- 局部注册(在 Controller 添加装饰器)
@Controller('person')
// @UseInterceptors(LoginGuard) // 当写在这里时,表明此拦截器将作用于该 Controller 下的所有 handler 内容
export class PersonController {
constructor(private readonly personService: PersonService) {}
@Get()
@UseInterceptors(LoginGuard) // 使用 @UseInterceptors 添加局部拦截器
findAll() {
return this.personService.findAll();
}
}
Pipe
管道的主要作用就是对参数进行校验和转换。Nest 中文网-管道
自定义管道
- 创建 pipe 管道模板
nest g pipe validate --no-spec --flat
- 对管道模板进行自定义的参数校验
@Injectable()
export class ValidatePipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
if (Number.isNaN(parseInt(value))) {
throw new BadRequestException(`参数${metadata.data}错误`);
}
return typeof value === 'number' ? value * 10 : parseInt(value) * 10;
}
}
- 创建一个请求
import { ValidatePipe } from 'src/validate.pipe';
@Controller('person')
export class PersonController {
constructor(private readonly personService: PersonService) {}
@Get('/add') // @Query 接收第二个参数,即刚才的自定义校验管道
addNum(@Query('num', ValidatePipe) num: number) {
return num + 1;
}
}
局部管道和全局管道
- 全局管道也是两种注册方式(app 和 AppModule)
- 局部管道
Nest 内置 Pipe
Nest 附带了九个开箱即用的管道
- ValidationPipe
- ValidationPipe 较为特殊,详细使用请查看中文网文档:参考链接
- ParseIntPipe
- ParseFloatPipe
- ParseBoolPipe
- ParseArrayPipe
- ParseUUIDPipe
- ParseEnumPipe
- DefaultValuePipe
- ParseFilePipe
简单使用
- ParseIntPipe 只允许整数 int 类型的数字通过校验
import { ParseIntPipe } from '@nestjs/common';
@Get('/add')
addNum(@Query('num', ParseIntPipe) num: number) {
return num + 1;
}
更多便捷参数验证请查看 Nest 中文网-管道#类验证器
自定义内置管道的内容
当需要返回自定义的错误状态码时,可以实例化内置管道
@Get('/add')
addNum( @Query('num', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }) ) num: number ) {
return num + 1;
}
ExceptionFilter
Nest 内置的异常层,所有未经处理的异常错误,该异常层就会进行捕获,并返回对应的响应信息,相当于内置的一个全局异常拦截器。Nest 中文网-异常过滤器
比如手动 throw new BadRequestException(...)
就会被默认内置的异常层捕获
nest g filter ex --no-spec --flat
对抛出的 BadRequestException
进行单独的异常过滤
import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common';
import { Request, Response } from 'express'; // Request 一定是从 express 中导出,而不是 @nestjs/common
@Catch(BadRequestException) // 捕获 BadRequestException 异常
export class HttpFilter implements ExceptionFilter {
catch(exception: BadRequestException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
response.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
局部使用该异常过滤器
@Get('/add')
@UseFilters(HttpFilter)
addNum(@Query('num', ValidatePipe) num: number) {
return num + 1;
}
http 相关的内置异常
- BadRequestException
- UnauthorizedException
- NotFoundException
- ForbiddenException
- NotAcceptableException
- RequestTimeoutException
- ConflictException
- GoneException
- PayloadTooLargeException
- UnsupportedMediaTypeException
- UnprocessableException
- InternalServerErrorException
- NotImplementedException
- BadGatewayException
- ServiceUnavailableException
- GatewayTimeoutException
也可以自行扩展,自定义异常
export class MyException extends HttpException {
constructor() {
super('Custom Error', 999);
}
}
全局注册和局部注册
- 全局异常过滤器的两种注册方式和管道、守卫等一致(app 和 AppModule)
// main.ts 中注册
app.useGlobalFilters(new HttpFilter());
// AppModule 中注册
@Module({
providers: [
AppService,
{
provide: APP_FILTER, // // 该常量由 @nestjs/core 导出
useClass: HttpFilter,
},
],
})
- 局部注册,可作用于单个 handler 指定异常,也可作用于整个 Controller 的 handler 指定异常
@Controller('person')
// @UseFilters(HttpFilter) 作用于该 controller 下的所有捕获到的指定异常
export class PersonController {
constructor(private readonly personService: PersonService) {}
@Get('/add')
@UseFilters(HttpFilter) // 只作用于该 handler 的指定异常
addNum(@Query('num', ValidatePipe) num: number) {
return num + 1;
}
}
AOP 的执行顺序
该图来自于 stackoverflow。原文地址
Middleware
是 Express 的概念,Nest 将其放在最外层执行。到了某个路由之后,会先调用 Guard
,Guard
用于判断路由有没有权限访问,然后会调用 Interceptor
,对 Contoller
前后扩展一些逻辑,在到达目标 Controller
之前,还会调用 Pipe
来对参数做检验和转换。在此期间,不论是 Pipe
校验抛出的异常或是其它所有的 HttpException 的异常都会被 ExceptionFilter
处理,最后返回不同的响应。