跳到主要内容

AOP

AOP(面向切面编程),传统的 MVC 框架,当我们想在请求处理前,做一些通用逻辑的处理,这时就需要侵入 Controller 层的代码,不管是开发还是维护都非常痛苦的,业务代码变得冗余臃肿。而 Nestjs 通过 AOP 能把一些通用逻辑抽离出去,通过装饰器的方式注册到 Controller 中,这样就更容易维护了,业务代码也就变得更纯粹了。

Nestjs 常用的有这几种:

  • MiddleWare:和 Express 中间件一样的洋葱模型,会在请求前后执行,这个就是为了兼容 Express 的中间件的
  • Guard:路由守卫,在访问 Controller 前做一些权限判断,返回 true 或 false
  • Pipe:对参数做一些校验
  • Interceptor:拦截器,会在请求处理前后执行一些逻辑
  • ExceptionFilter:对应用的错误进行拦截,并返回错误信息

调用顺序

  • 首先调用 MiddleWare,到达某个路由
  • 调用 Guard,判断有没有访问路由的权限
  • 调用 Interceptor 对 Controller 做一些扩展逻辑的处理
  • 调用 Pipe 对参数进行校验
  • 调用 Controller handle
  • 如果整个请求处理周期出现做错,会被 ExceptionFilter 拦截并响应错误信息

MiddleWare​

这个是 Express 中的概念,Nestjs 是 Express 实现的,自然支持,但在开发中用的比较少,语义不强

中间件分为全局中间件和路由中间件

全局中间件​

Nestjs 并没有提供全局中间件的 useGlobalXxx 方式,所以可以使用 app.use 来在 main 程序入口注册

main.ts
app.use((req, res, next) => {
console.log('全局中间件 before');
next();
console.log('全局中间件 after');
});

路由中间件​

使用命令生成一个 log 中间件

$ nest g middleware middlewares/log --no-spec

必须在 app.module.ts 中注册,配置哪些路由可使用

app.module.ts
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LogMiddleware).forRoutes('user');
}
}
访问 /user/aaa,输出结果
全局中间件 before
路由中间件 before /aaa
handler
路由中间件 after
全局中间件 after

Guard​

到达路由时,Guard 会判断有没有权限访问,返回 true 放行,返回 false 响应客户端 403

Guard 也分为全局 Guard 和路由级Guard

$ nest g guard guards/role --no-spec
./src/guards/role.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class RoleGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const req = context.switchToHttp().getRequest<Request>();
console.log('guard');
if (req.url === '/user/bbb') {
return false;
}
return true;
}
}

路由 Guard​

使用 UseGuards 装饰器

@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}

@Get('/aaa')
@UseGuards(RoleGuard)
findAll() {
console.log('handler');
return this.userService.findAll();
}

@Get('/bbb')
@UseGuards(RoleGuard)
findOne(@Param('id') id: string) {
console.log('handler');
return this.userService.findOne(+id);
}
}

输出结果
  1. 访问 /user/aaa
guard
handler
  1. 访问 /user/bbb
guard

响应 403

response 403

全局使用​

nestjs 提供了两种注册全局 Guard 的方式,区别在于是否可以使用 IOC 容器内的 provider

  1. 是在 IOC 容器内注册,优势是可以使用 IOC 容器内的 Provider,比如在 Guard 内可以注入使用 AppService
app.module.ts
import { RolesGuard } from './guard/role-guard';

@Module({
imports: [],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class AppModule implements NestModule {
}
  1. 全局 use
main.ts
import { RoleGuard } from './guard/role.guard';

async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);

app.useGlobalGuards(new RoleGuard());

await app.listen(8080);
}
bootstrap();

Interceptor​

拦截器,目的实在 COntroller 前后执行一些逻辑

$ nest g interceptor interceptors/test --no-spec
@Injectable()
export class RequestInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> {
const req = context.switchToHttp().getRequest<Request>();
console.log('interceptor before');
return next.handle().pipe(
tap((data) => {
console.log('interceptor after', req.url, data);
return 'o';
}),
catchError((error) => {
console.log('qwewq', error);
return [error];
}),
);
}
}

全局使用方式、区别和 Guard 类似,也分为两种

  • 在 main.ts 使用 useGlobalInterceptors
  • 在 app.module.ts 使用 APP_INTERCEPTOR

Pipe​

执行 Controller 前,对请求参数进行校验和转换

$ nest g pipe pipes/verify-value --no-spec

后端拿到 query 的数据,每个属性值其实都是一个字符串,这时我们可以用 pipe 将属性值转换为我们想要的数据类型

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

@Injectable()
export class VerifyValuePipe implements PipeTransform {
transform(values: any, metadata: ArgumentMetadata) {
// data 有值,证明取得是某一个属性,data 为属性 key
const { type, data } = metadata;
if (type === 'query') {
if (!data) {
Object.entries(values).forEach(([key, value]) => {
if (value === 'true') {
values[key] = true;
} else if (value === 'false') {
values[key] = false;
} else if (!Number.isNaN(Number(value))) {
values[key] = Number(value);
}
});
}
if(data === 'a') {
values = Number(value)
}
}
return values;
}
}

路由使用​

如果 findAll 方法有多个参数时,会多次调用 VerifyValuePipe 对每个参数做校验

@Get('/aaa')
@UseGuards(RoleGuard)
@UsePipes(VerifyValuePipe)
findAll(@Query() query: any) {
console.log('handler', query);
return this.userService.findAll();
}

也可以对某一个参数做 pipe

@Get('/aaa')
@UseGuards(RoleGuard)
findAll(@Query("a", VerifyValuePipe) a: number) {
console.log('handler', a);
return this.userService.findAll();
}

全局注册​

和 Guard、Interceptor 类似

  • 在 main.ts 使用 useGlobalPipes
  • 在 app.module.ts 使用 `APP_PIPE

ExceptionFilter​

ExceptionFilter 会捕捉到应用程序抛出的异常,Nestjs 提供了一些内置的 ExceptionFilter,比如前面的 Guard 返回 403,返回错误信息是被 ForbiddenException 处理过的

内置的 Exception 都是 HttpException 的子类,HttpException 是继承 Error 对象的,

$ nest g filter filters/test --no-spec
import {
ArgumentsHost,
BadRequestException,
Catch,
ExceptionFilter,
} from '@nestjs/common';
import { Response } from 'express';

@Catch(BadRequestException)
export class TestFilter implements ExceptionFilter {
catch(exception: BadRequestException, host: ArgumentsHost) {
const response: Response = host.switchToHttp().getResponse<Response>();

response.status(400).json({
statusCode: 400,
message: 'test: ' + exception.message,
});
}
}

使用方式和 Guard、Interceptor 类似

注意的点​

MiddleWare 尽量不要与 Guard、Interceptor 一同使用,可以看一下一起使用有什么问题

MiddleWare 的后置逻辑会在 Guard 执行后在执行,而我们期望的应该是在 Interceptor 执行后执行

如果不用 Guard,就会是这样

所以在 Nestjs 尽量不要使用 MiddleWare;如果非要使用的话,就不要写后置逻辑,或者不用 Guard、Interceptor,只使用 MiddleWare