记录我的NestJS探究历程(六)

在上一篇文章中分析了NestJS的中间件的知识点之后,本文开始分析NestJS过滤器知识点。

初探NestJS的运行原理之过滤器

1、基本使用

在NestJS中,过滤器的定位是用于处理未手动捕获的异常,以下是摘录自它的官网的描述:

Nest comes with a built-in exceptions layer which is responsible for processing all unhandled exceptions across an application. When an exception is not handled by your application code, it is caught by this layer, which then automatically sends an appropriate user-friendly response.

因此,后文我们统一把它称为异常过滤器

在服务端处理的过程中,我们很多时候都需要抛出异常,这种异常是不能乱抛的,因为前端往往需要真实的错误信息,如果不论什么错误都给前端抛500,server internal error是不负责任的,实际开发中要是发生一个线上事故那可能会把人找的头皮发麻,压力山大。

所以,在NestJS中定义了一系列的错误信息:

这些内建的错误信息都是对标准HTTP错误的实现。

比如,官网上给的例子:

ts 复制代码
@Get()
async findAll() {
  throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}

在实际的业务处理中,我们一般前后端在接口请求的时候,后端一般不会给前端报Http错误,正常的项目开发中,后端一般的做法都是给前端返回HttpCode为200,然后约定一个业务用的errercode,前端根据这个errorcode给予用户相应的错误提示。

因此,基于这种场景下,我们可以实现一个这样的通用业务错误,在任何非预期的业务错误场景下,直接抛出这个错误即可。

ts 复制代码
export class CustomException extends Error {
  // 业务错误的code
  public code: number;
  // 返回给前端的HttpStatus,一般都给200
  public httpStatus: number;
  
  constructor(message: string, code: number, status = 200) {
    super();
    this.message = message;
    this.code = code;
    this.httpStatus = status;
  }
}

然后,在绑定全局错误过滤器的时候,就可以判断是否是这类错误,是的话给前端提示具体的错误信息,不是的话就只能提示通用的错误信息了,比如500,server internal error(得把服务端真实的报错信息隐藏起来,以免被别有用心的人攻击)。

以下是我项目中的一个示例:

ts 复制代码
@Catch()
export class StandardErrorCaptureExceptionFilter extends BaseExceptionFilter {
 
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    let code: number;
    let status = HttpStatus.INTERNAL_SERVER_ERROR;
    let message = 'server internal error';
    if (exception instanceof HttpException) {
      status = exception.getStatus();
      message = exception.message;
    } else {
      message = exception.message;
    }
    // 此刻一般还会进行日志的记录,示例代码省略了。
    response.status(status).json({
      code,
      msg: message,
      data,
    });
  }
}

最后,在程序的入口处进行绑定。

ts 复制代码
@Module({
  imports: [],
  providers: [
    {
      provide: APP_FILTER,
      useClass: StandardErrorCaptureExceptionFilter,
    },
  ],
})
export class AppModule {

}

或者使用官方提供的API手动绑定。

ts 复制代码
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new StandardErrorCaptureExceptionFilter());
  await app.listen(3000);
}
bootstrap();

另外,还有针对某个控制器或方法进行绑定的异常过滤器(使用UseFilters装饰器),此处就不赘述了,在原理分析章节我们看一下它的应用过程即可。

2、原理分析

添加过滤器的过程

在知道怎么用异常过滤器之后,我们开始分析其原理。

首先看一下Catch装饰器到底干了一个什么事儿,它的源码在packages/common/decorators/catch.decorator.ts处,以下是它的实现:

ts 复制代码
export function Catch(
  ...exceptions: Array<Type<any> | Abstract<any>>
): ClassDecorator {
  return (target: object) => {
    Reflect.defineMetadata(CATCH_WATERMARK, true, target);
    Reflect.defineMetadata(FILTER_CATCH_EXCEPTIONS, exceptions, target);
  };
}

从这个装饰器的定义可以看的出来,似乎Catch接受的参数是它需要捕获的异常类型,如果没有传递参数的话,应该就是捕获所有的异常了。

看完了这个方法的实现以后,我们开始看一下NestJS的useGlobalFilters方法干了啥。 看起来,什么都没有干的样子呀,线索似乎断了,别急,我们先看一下我们之前给出的第一种异常过滤器的绑定方式,先在它的源码仓库里面搜索一下APP_FILTER 在源码里面打个断点,查看一下堆栈调用信息,就知道它是什么应用的了。 在之前的文章,我们简单的先分析了NestJS的IoC容器,以及DI的过程。在这个堆栈调用信息我们能抓住一个关键,就是当模块完成创建之后,便开始处理这些有特殊意义的Provider。 这种方法,加入异常过滤器的方式和之前我们手动调用是几乎一样的,唯一的差别就是,使用的是IoC容器来创建的异常过滤器的实例。

以上流程,过滤器的添加逻辑就已经完成了,过滤器什么时候开始工作的,咱们需要接着往下看。

过滤器的工作流程

不用说,我们会首先把目光放回之前路由的章节,因为在分析路由的处理逻辑时,里面其实有提及异常过滤器了,只不过当时我们的重心在路由,因此就没有考虑异常过滤器。

我们把目光聚焦回packages/core/router/router-resolver.ts这个位置,还记得这个类的功能是什么吗,这个类负责的是把Controller的方法跟定义的路由Path关联起来,如果忘记了的话,可以查看我的这个专题的第4篇文章。

在这个类中初始化了RouterExceptionFilters这个类,可以看到传入的是ApplicationConfig,在之前注册的时候,我们的全局异常过滤器就是挂在ApplicationConfig上的哦。 然后在RouterExplorer这个类中绑定注册的异常过滤器。 异常过滤器的工作逻辑大概已经明白了,最后需要看一下RouterExceptionFilters这个类到底负责了什么逻辑。 接着,看一下ExceptionsHandler,它继承自BaseExceptionFilter,这个位置,是可以为大家解答一个疑惑的,我们使用NestJS脚手架创建的项目,什么都不做,当访问出错的时候,并不会报错,而是得到了一个格式化的错误信息 ,这其中的魔法就在这个位置了。 此刻,我们应该能提出疑问了,当我们编写了自定义的异常过滤器,为什么错误就被我们自定义的异常过滤器捕获而不是走的通用逻辑?还有就是我们在Catch装饰器捕获异常的范围它是怎么处理的呢?,带着这两个问题,我们接着往下看。

另外,还有一个点可以留意一下,在前文我向大家给出的我自定义的异常过滤器,继承的父类就是这个BaseExceptionFilter哦。

接着,查看createContext方法的定义,可以看到,它反射了class上定义的元数据和方法上定义的元数据。

找出所有的过滤器,分别是得到的顺序是:全局过滤器->Controller过滤器->方法过滤器,请记住这个顺序,一会儿会知道这个顺序的意义。 在上面的截图中,createConcreteContext方法是一个抽象方法,它是被BaseExceptionFilterContext实现的。 getGlobalMetadata又是被RouterExceptionFilters重写的。 createConcreteContext是为了得到全部的异常过滤器,之前我们已经提到过全局异常过滤器的注册方式有两种,一种是通过useGlobalFilters,手动传入一个实例化的异常过滤器进去;另外一种方式就是作为Provider的形式,让IoC容器帮忙创建。 于是就得到了全局的过滤器,并且知道每个异常过滤器应该负责的错误类型。 此时,我们已经能解答之前的一个疑问了,就是我们如果没有自定义过滤器,NestJS为什么会给我们捕获,如果自定义了过滤器,则进入到自定义过滤器的逻辑。 还差一个疑问没有得到解决,就是每个异常过滤器怎么只处理它所关心的错误的。

先全局搜一下exceptionMetatypes,应该是能找到一些线索的,过滤有用的信息,是在这个方法里面处理的。 先看一下这个函数的功能是什么,看起来就是找一个仅处理对应错误类型的过滤器,然后,看一下这个方法是在什么位置被调用的。 我们先捋一捋上面的逻辑:如果能找的到预期错误类型的自定义错误处理器,则应用自定义的错误处理器,如果找不到的话,则应用NestJS内置的兜底异常过滤器,那这样就防止了用户如果注册的过滤器如果全部都是自定义过滤器并且仅处理特征类型的错误,没有一个兜底处理未知类型错误的过滤器的问题。

最后,还有一个重要的知识点,需要留意一下,在之前,找自定义过滤器的过程中,先找的是全局过滤器,接着找的是控制器过滤器,最后找的是方法过滤器,此刻,将顺序颠倒过来,为的是保证顺序的优先级,肯定是小范围到大范围嘛,所以是最先执行方法过滤器,控制器过滤器其次,最后是全局过滤器

至此,异常过滤器我们就已经明白它的注册和运行逻辑了。

总结

  • 1、NestJS的过滤器只有一个进行错误处理的用途。
  • 2、使用useGlobalFilters方法挂载的全局过滤器和使用providers选项挂载的全局过滤器大致是没有区别的(本文没有研究注入的scope选项),只不过useGlobalFilters需要你自己初始化过滤器,而用注入的方式,初始化的过程交给了NestJS的IoC容器。
  • 3、NestJS的过滤器的优先顺序是最先执行方法过滤器,控制器过滤器其次,最后是全局过滤器。

NestJS的过滤器的定位跟Spring MVC的过滤器的定位区别还是很大的,在Spring MVC中,过滤器可以做很多的事儿,比如:

  1. 请求预处理: 过滤器可以在请求到达控制器之前执行,用于进行一些预处理逻辑,例如设置请求的字符编码、对请求进行验证、记录请求日志等。
  2. 身份验证与授权: 过滤器可以用于进行身份验证和授权,检查用户是否具有执行请求操作的权限,以及在需要时执行登录操作。
  3. 日志记录: 过滤器可以用于记录请求和响应的信息,帮助进行调试或者生成访问日志。
  4. 请求修改: 过滤器可以修改请求的内容,例如解压缩请求数据、加密请求参数等。
  5. 响应修改: 过滤器可以修改响应的内容,例如压缩响应数据、加密响应内容等。
  6. 性能监控: 过滤器可以用于收集应用程序性能数据,例如请求处理时间、资源消耗等,以便进行性能监控和调优。

为此,大家需要注意它们的区别。

在下一节内容,我们将开始分析NestJS的拦截器,守卫等内容的技术细节,敬请期待!

相关推荐
她似晚风般温柔7894 分钟前
Uniapp + Vue3 + Vite +Uview + Pinia 分商家实现购物车功能(最新附源码保姆级)
开发语言·javascript·uni-app
Jiaberrr1 小时前
前端实战:使用JS和Canvas实现运算图形验证码(uniapp、微信小程序同样可用)
前端·javascript·vue.js·微信小程序·uni-app
everyStudy2 小时前
JS中判断字符串中是否包含指定字符
开发语言·前端·javascript
Ylucius2 小时前
动态语言? 静态语言? ------区别何在?java,js,c,c++,python分给是静态or动态语言?
java·c语言·javascript·c++·python·学习
200不是二百2 小时前
Vuex详解
前端·javascript·vue.js
LvManBa2 小时前
Vue学习记录之三(ref全家桶)
javascript·vue.js·学习
深情废杨杨2 小时前
前端vue-父传子
前端·javascript·vue.js
司篂篂4 小时前
axios二次封装
前端·javascript·vue.js
姚*鸿的博客4 小时前
pinia在vue3中的使用
前端·javascript·vue.js
Jiaberrr6 小时前
JS实现树形结构数据中特定节点及其子节点显示属性设置的技巧(可用于树形节点过滤筛选)
前端·javascript·tree·树形·过滤筛选