Nestjs Aop 切片

除了 middleware ,nestjs 提供了 4 种用于 AOP 机制的切片,帮助我们处理实际中遇到的非常具体的问题。

Exception Filters :异常拦截器

应用场景: 拦截特定异常,返回友好文案

  • exceptions layer

nest 内置了一个异常捕捉层,用于处理应用中未处理的异常。作用是为了避免异常没有处理导致应用崩溃,同时返回用户友好的文案,默认会返回如下的文案。

json 复制代码
{
  "statusCode": 500,
  "message": "Internal server error"
}
  • HttpException : 内置 Http 异常,被 nest 拦截

HttpException 是 nest 内置的用于抛出 http 请求相关的异常。 可以直接抛出 throw new HttpException('Forbidden', HttpStatus.FORBIDDEN); 。 也可以继承 HttpException 自定义异常类。

针对具体的 http 异常,nest 也提供了 BadRequestException,NotFoundException,BadGatewayException 等类。

  • Exception filters : 自定义 filter ,通过 @Catch 指定拦截异常 , 通过 @UseFilters 绑定到 controller

除了内置,可以定义一个 Exception filter 来处理某个特定类型的错误。

  • 自定义 filter , 实现一个错误码

这里用一个错误码 filter 为例, 创建一个 Exception filter

ts 复制代码
// 1、先创建 ErrorCodeException  // errorCode.exception.ts
type Lang = 'zh-CN' | 'en-GB';
const ErrorCodeMap: Record<string, Record<Lang, string>> = {
  '90000001': {
    'zh-CN': '系统异常',
    'en-GB': 'System Error',
  },
  '90000002': {
    'zh-CN': '系统异常 2',
    'en-GB': 'System Error 2',
  },
  '90000003': {
    'zh-CN': '系统异常 3',
    'en-GB': 'System Error 3',
  },
  '90000004': {
    'zh-CN': '参数错误 4',
    'en-GB': 'Argument Error 4',
  },
};
export class ErrorCodeException {
  code = '90000001';
  message = 'System Error';

  constructor(code: string) {
    this.code = code;
  }

  async getMessage(lang: Lang = 'zh-CN') {
    // 后续从错误码系统拉取
    return Promise.resolve(ErrorCodeMap[this.code][lang]);
  }
}
//2、创建 ErrorCodeFilter // errorCode.filters.ts
import { ArgumentsHost, Catch, HttpStatus } from '@nestjs/common';
import { ErrorCodeException } from './errorCode.exception';
import { Response, Request } from 'express';

@Catch(ErrorCodeException)
export class ErrorCodeFilter {
  async catch(exception: ErrorCodeException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const message = await exception.getMessage(request.cookies.lang);

    response.status(HttpStatus.OK).json({
      code: Number(exception.code),
      message: message,
    });
  }
}

// 3、在 Controller 中绑定 filter
@Get()
@UseFilters(new ErrorCodeFilter())
findAll() {
    throw new ErrorCodeException('90000003');
}

Pipes :请求参数管道

应用场景: 校验请求参数,拦截异常接口。 或者 在 controller 之前加工请求参数

  • Custom pipes : 创建一个类,用 @Injectable() 声明,实现 PipeTransform, 用 @UsePipes()注入 或者 app.useGlobalPipes() 全局引用。

  • ArgumentMetadata : 参数类型的元数据;这里是运行时数据 如果类型是 ts interface, metatypeundefined 。如果是类,就是就是其构造函数

ts 复制代码
export interface ArgumentMetadata {
  type: 'body' | 'query' | 'param' | 'custom';
  metatype?: Type<unknown>;
  data?: string;
}
  • 使用 zod 创建一个接口参数拦截器
ts 复制代码
// 1、使用 zod 定义参数类 , 和 zod schema
import { z } from 'zod';

export class IUser {
  static zodSchema = z.object({
    name: z.string(),
    age: z.number().optional(),
  });

  name: string;
  age?: number;
}
// 2 、给 controller 参数指定类型
@Post()
create(@Body() user: IUser) {
  return this.homeService.create(user);
}
// 3、自定义 pipe
import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';
import { ErrorCodeException } from 'src/ErrorCode/errorCode.exception';
import { ZodSchema } from 'zod';

@Injectable()
export class ParamsValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    try {
      const metatype: any = metadata.metatype;
      if (metatype.zodSchema instanceof ZodSchema) {
        // 获取 schema 并校验
        const parsedValue = metatype.zodSchema.parse(value);
        return parsedValue;
      }
      return value;
    } catch (error) {
      // 会被 ErrorCodeFilter 拦截,并返回翻译后的文案
      throw new ErrorCodeException('90000004');
    }
  }
}
// 4、全局使用
app.useGlobalPipes(new ParamsValidationPipe());
  • 使用内置 ValidationPipe 校验参数类型格式
ts 复制代码
// 1、安装依赖 pnpm add class-validator class-transformer
// 2、使用装饰器描述类的属性
import { IsNumber, IsOptional, IsString } from 'class-validator';

export class IUser {
  @IsString()
  name: string;

  @IsNumber()
  @IsOptional()
  age?: number;
}
// 3、全局引用 Pipe
app.useGlobalPipes(new ValidationPipe());
// 4、校验不过返回 BadRequestException
{
    "message": [
        "name must be a string"
    ],
    "error": "Bad Request",
    "statusCode": 400
}
// 5、自定义返回内容 // ValidationPipe 构造函数添加 exceptionFactory 方法
app.useGlobalPipes(
  new ValidationPipe({
    exceptionFactory: (errors) => {
      return new BadRequestException({
        code: HttpStatus.BAD_REQUEST,
        message: `argument ${errors[0].property} is wrong `,
      });
    },
  }),
);
// 返回内容
{
    "code": 400,
    "message": "argument name is wrong "
}

Guards : 请求守卫

应用场景:拦截异常请求,一般是校验 cookie header 参数 等

  • Guards : 就是一个用@Injectable() 声明的类,并实现了 CanActivate 接口 。 使用 @UseGuards() 或者 app.useGlobalGuards() 绑定 或者 全局绑定 . 拦截之后会抛出 ForbiddenException 异常

  • Execution context:

执行上下文,从 ArgumentsHost 继承,可以从上下文中获取 request,response 等数据。

  • 创建一个 Guard 来校验 cookie 中 sessionId 判断是否有登陆
ts 复制代码
// 1、安装 cookie-parser : pnpm add cookie-parser
// 2、使用 cookie-parser middleware
app.use(CookieParser());
// 3、创建 guard : nest g -g --no-spec
import {
  CanActivate,
  ExecutionContext,
  ForbiddenException,
  Injectable,
} from '@nestjs/common';
import { Observable } from 'rxjs';

const checkLogin = async (sessionId: string) => {
  // 校验接口判断
  throw new ForbiddenException('login Expired');
};

@Injectable()
export class LoginGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    // context.getType 'http' | 'ws' | 'rpc'
    const req = context.switchToHttp().getRequest();
    const { sessionId } = req.cookies;
    if (!/^[0-9a-z]{10}/g.test(sessionId)) {
      throw new ForbiddenException('unlogin');
      // return false; // 抛出 ForbiddenException 异常
    }
    return checkLogin(sessionId);
  }
}

Interceptors : 拦截器

应用场景: 在 controller 执行过程 前、后 添加逻辑。 类似 middleware 的逻辑 ,遵循洋葱模型,也分 前逻辑后逻辑

  • 对比 middleware : 多了 Execution context, 可以获取更多的信息, 能做的事情更多,更能体现 AOP 的概率。

  • 用法:创建类,用 @Injectable() 声明并实现 NestInterceptor 接口 , 使用 @UseInterceptors()app.useGlobalInterceptors() 绑定。 绑定多个拦截器,注意顺序。

  • 创建三个拦截器,包括返回数据加工 ,加入 traceId 追踪, 并打印日志

ts 复制代码
// 1、创建数据加工拦截器 transform.interceptor.ts
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { Observable, map } from 'rxjs';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    // 加工返回数据,只有 `后逻辑`
    return next.handle().pipe(
      map((data) => {
        return {
          code: 0,
          data,
          message: 'success',
        };
      }),
    );
  }
}
// 2、创建追踪拦截器 trace.interceptor.ts
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { Observable, map } from 'rxjs';

@Injectable()
export class TraceInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    // 前逻辑,创建 traceId ,后续使用 zipkin
    const traceId = Math.random().toString(16).slice(2);
    return next.handle().pipe(
      map((data) => {
        // 后逻辑:把 traceId 加入到返回结果
        return {
          ...data,
          tid: traceId,
        };
      }),
    );
  }
}
// 3、创建日志拦截器 log.interceptor.ts
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { Request } from 'express';
import { Observable, tap } from 'rxjs';

@Injectable()
export class LogInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest<Request>();
    // 前逻辑,打印入口日志
    const startTime = Date.now();
    console.log({
      url: request.url,
      cookie: request.cookies,
      date: startTime,
    });
    return next.handle().pipe(
      tap((data) => {
        // 后逻辑,打印出口日志,包括接口耗时,这里第一个绑定拦截器,保证 后逻辑 是最后的执行逻辑
        console.log({
          url: request.url,
          data,
          time: Date.now() - startTime,
        });
      }),
    );
  }
}
// 4、绑定这3个拦截,注意顺序
app.useGlobalInterceptors(new LogInterceptor());
app.useGlobalInterceptors(new TraceInterceptor());
app.useGlobalInterceptors(new TransformInterceptor());
// 5、controller 
@Post()
create(@Body() user: IUser) {
  return user;
}
// 6、测试接口 , body 设置为 json ,
{
    "name": "light",
    "age": 18,
    "sex": "male"
}
// 6、接口返回数据
{
    "code": 0,
    "data": {
        "name": "light",
        "age": 18,
        "sex": "male"
    },
    "message": "success",
    "tid": "252f4d9067279"
}

总结

一个完整的 nest web 应用除了处理业务逻辑,还需要应对许多其他的场景。上面用 nest 提供的 4 中 AOP 切片,分别处理相对具体的场景。

  • Exception Filters : 创建了一个错误码解析器,同时还具备多语言的功能。 用于业务异常的抛出,前端直接提示给用户即可
  • Pipes : 创建了一个提交接口的参数类型校验器,用于拦截非法数据提交。
  • Guards : 创建了一个回话态校验逻辑,用户每次的提交都需要校验,后台在校验的时候,如果未过期,要注意续期
  • Interceptors : 创建了3个拦截器,分别用于数据加工,traceId , 日志。 用于统一返回数据格式,和业务链路上报。
相关推荐
小行星1258 分钟前
前端预览pdf文件流
前端·javascript·vue.js
小行星12515 分钟前
前端把dom页面转为pdf文件下载和弹窗预览
前端·javascript·vue.js·pdf
Lysun00124 分钟前
[less] Operation on an invalid type
前端·vue·less·sass·scss
J总裁的小芒果39 分钟前
Vue3 el-table 默认选中 传入的数组
前端·javascript·elementui·typescript
Lei_zhen9642 分钟前
记录一次electron-builder报错ENOENT: no such file or directory, rename xxxx的问题
前端·javascript·electron
咖喱鱼蛋44 分钟前
Electron一些概念理解
前端·javascript·electron
yqcoder1 小时前
Vue3 + Vite + Electron + TS 项目构建
前端·javascript·vue.js
鑫宝Code1 小时前
【React】React Router:深入理解前端路由的工作原理
前端·react.js·前端框架
Mr_Xuhhh2 小时前
重生之我在学环境变量
linux·运维·服务器·前端·chrome·算法
永乐春秋3 小时前
WEB攻防-通用漏洞&文件上传&js验证&mime&user.ini&语言特性
前端