likes
comments
collection
share

记录我的NestJS探究历程(九)——管道

作者站长头像
站长
· 阅读数 2

在上一篇文章中分析了NestJS的守卫的知识点之后,本文开始分析NestJS的管道的知识点。

本文是一套系列文章,有很强的前后联系,如果您对NestJS感兴趣的话,建议您从本系列的开头开始阅读。

初探NestJS的运行原理之管道

我在读大学的时候曾经学过2年的ASP.NET,微软就喜欢搞管道这种东西,然后程序员根据自己的业务需求,重写某个管道的方法,好处就是简单,使得程序员更容易上手,开发效率也高。但是坏处就是因为很多东西都是预先自定义好的,要是想自定义的话,就显得比较困难。

NestJS中的管道跟ASP.NET中的管道完全是两个概念,ASP.NET中的管道类似于NestJS中间件的概念,而NestJS的管道是一个数据的转换或验证工具。

首先,我们要明白,NestJS为什么会提供这样的能力,对于数据的验证,想必大家都知道吧,因为前端所处环境是直接面对用户,我们的用户究竟是什么样的情况是完全不知道的,对于有些别有用心的用户就希望我们的系统宕机,有可能给你提交一堆乱七八糟的东西到服务器,如果我们直接处理的话,服务器就容易遭受攻击。

有的新手程序员会说,数据有效性验证这种场景,前端不是会做吗,既然前端做了,那服务端再做不是浪费时间嘛。这种观点是大错特错的,正所谓防君子防不了小人,如果用户把你的请求给你记下来,然后编程去请求,就比如我们在查bug的时候,经常会在浏览器上copy一个请求的curl,然后服务端的同学就可以直接请求,进而查找问题。 记录我的NestJS探究历程(九)——管道

所以,正因为这样,服务端必须要进行数据的校验,而数据的校验是脏活累活,所以对这些验证流程进行统一的封装的话,能够使得我们的代码更好的组织,提高系统的可维护性。

NestJS官方文档上说管道的用途是这样的。

Pipes have two typical use cases:

  • transformation: transform input data to the desired form (e.g., from string to integer)
  • validation: evaluate input data and if valid, simply pass it through unchanged; otherwise, throw an exception

从我个人的项目实践来看的话,转化数据这个场景我倒是遇到的少,进行数据的验证的场景倒是挺多的,所以我就以验证的场景来给大家举例子吧。

另外,使用自定义装饰器也能实现验证或者数据转化的这类操作,因此大家就萝卜青菜各取所爱吧,反正你觉得哪个API你看着舒服,就好,哈哈哈。

在NestJS中使用管道

给大家我一个实际的项目例子,我们的运营活动是作为内嵌h5运行在公司的App内,对于需要鉴权的接口,需要用户在请求的时候提供两个参数userId和token(但不是所有的接口都需要做鉴权)

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

type AuthInfo = { userId: string; token: string };

export class AuthValidationPipe implements PipeTransform<AuthInfo> {
  transform(value: AuthInfo, metadata: ArgumentMetadata): AuthInfo {
    // 校验用户的id是否是一个合法的电话号码
    const checkExp = /^(\+86-)?1[3456789]\d{9}$/;
    if (!checkExp.test(value.userId)) {
      throw new BadRequestException(HttpStatus.BAD_REQUEST);
    }
    return value;
  }
}

然后在业务侧挂载它。

import { Body, Controller, Post } from '@nestjs/common';
import { AuthValidationPipe } from './auth-validation.pipe';

@Controller('/reward')
export class RewardController {
  @Post('/draw')
  requestLottery(
    @Body(AuthValidationPipe) userInfo: { userId: string; token: string },
  ) {
    //
  }
}

当用户请求接口的时候,如果不满足我们预期的数据结构,将会提前返回错误信息给用户。 记录我的NestJS探究历程(九)——管道 另外再给大家举一个用管道进行数据转化的例子,在某些时候,前后端约定数据结构传递的时候,对于Get操作的接口,大家不喜欢用数组接收,而是传递一个以逗号分隔的字符串(主要是为了简便,不容易出错),这种场景下就可以使用管道将其转化成数组处理。

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

export class ListTransformPipe implements PipeTransform<string, string[]> {
  transform(value: string, metadata: ArgumentMetadata) {
    return value.split(',').filter(Boolean);
  }
}

在对应的方法处挂载。

@Controller('/reward')
export class RewardController {
  @Get('/record')
  getRecords(@Query('list', ListTransformPipe) list: string[]) {
    console.log(list);
  }
}

管道的注册方式要比之前我们聊过的拦截器,过滤器,守卫要多一种方式。

首先,就是我们都已经非常熟悉的方式,使用API进行全局挂载。

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}

第二种方式,也是跟之前的一样的,使用IoC容器的方式初始化。

import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_PIPE,
      useClass: ValidationPipe,
    },
  ],
})
export class AppModule {}

第三种方式,仍然是之前聊过的类同的方式,使用UsePipes,可以作用在控制器上,也可以作用在方法上,以下是官网的一个例子。

@Post()
@UsePipes(new ZodValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

最后是一个特殊的例子,BodyQueryParam这类装饰器能够接收一个或多个管道作为参数,我在项目中,用的就是这种方式。

@Controller('/reward')
export class RewardController {
  @Get('/record')
  getRecords(@Query('list', ListTransformPipe) list: string[]) {
    console.log(list);
  }
}

说完了管道的使用方式,接下来我们来分析一下它的运行原理。

管道的运行原理

上文我们提到过,管道比拦截器,过滤器多一种挂载方式,所以我们得看一下BodyQueryParam这类装饰器做了什么。

可以看到,装饰器内部把参数的顺序和管道进行了关联,绑定在了路由元数据上。 记录我的NestJS探究历程(九)——管道 记录对应位置的参数的管道 记录我的NestJS探究历程(九)——管道

关于全局管道的注册,本文就不再赘述。我们之前已经聊过了,和拦截器、过滤器一样,都是将其加入到ApplicationConfig这个类上暂存。因为使用的是模板方法模式,父类将通用行为进行了抽象,然后对应的子类根据自己所负责的业务重写父类的行为,最后,RouterExcecutionContext中去读取预期的管道以应用。

所以,我们可以直接把目光聚焦到之前已经关注过的这个位置: 记录我的NestJS探究历程(九)——管道 跟拦截器一样,管道的创建者和消费者,也是在RouterExplorer这个类中创建的。 记录我的NestJS探究历程(九)——管道 为大家捋一下这个位置的逻辑,createContext是父类的通用方法,这个方法调用的内容是由子类重写过的,可以完成特征的业务逻辑,见下面的2张截图 记录我的NestJS探究历程(九)——管道 记录我的NestJS探究历程(九)——管道 以下是读取全局的管道的逻辑。 记录我的NestJS探究历程(九)——管道 记录我的NestJS探究历程(九)——管道 接下来就可以真正的看一下管道是怎么样工作的了(在这个位置读取的管道还是跟过滤器、拦截器那儿所配置的是一样的,还没有到我们之前提到的Query,Param,Body这类装饰器上应用的管道)。 记录我的NestJS探究历程(九)——管道 得先看一下这个createPipeFn做了什么事儿。 paramsOptions是外界传入的参数,定义了一个pipesFn,它的能力就是对所有的paramsOptions做转换,所以,resolveParamValue函数的入参就是单个的paramsOption,我们还得把目光聚焦到它创建的位置。 记录我的NestJS探究历程(九)——管道 好,我们接着就看paramsOption的来源。 记录我的NestJS探究历程(九)——管道 getParamsMetadata这个函数来源于getMetadata里。 记录我的NestJS探究历程(九)——管道 看起来,又是什么关键线索都得不到,只得继续探索了,😭,看看exchangeKeysForValues这个方法又在做什么吧。

之前我们在阐述路由的时候聊过这个方法,这儿是在做参数的映射,就是怎么把Request对象、Response对象、Next函数映射成控制器方法对应的参数的逻辑。 记录我的NestJS探究历程(九)——管道 为什么我要把if的逻辑折起来了,在路由参数的时候我们聊过,这个是针对的自定义装饰器,为了简便起见,就可以简便分析啦。

至此,我们基本已经清楚NestJS的管道逻辑了,getMetdata方法这一系列的操作,都是在为拿Query、Param、Body这类装饰器挂载的管道做准备。好了,我们还需要回到之前的位置,不过,此刻我们已经不再疑惑了。 记录我的NestJS探究历程(九)——管道 所有的管道都组装好了,等待被PipesConsumer消费: 记录我的NestJS探究历程(九)——管道 最后,别忘了哦,我们一直聊的create方法,是被包裹在一个try-catch代码中执行的,所以,一旦管道执行的过程中出现错误,自动就被过滤器捕获,然后走异常逻辑了。 记录我的NestJS探究历程(九)——管道

我们来总结一下流程,首先,全局(useGlobalPipes添加或者IoC容器管理的)和UsePipes装饰器挂载的管道先解析到,然后再尝试解析Query、Body、Param这类装饰器挂载的管道,接着所有的管道进行合并,最终以异步调用链的形式应用管道。

总结

  • NestJS的管道定位是做数据的转化和校验
  • NestJS的管道的执行顺序是全局管道->控制器管道->方法管道->参数装饰器管道

NestJS的中间件、过滤器、拦截器、守卫、管道的学习总结

在这几个功能控件中,过滤器像是一个超脱世俗的贤者,它并不参与请求的流程处理,只不过在大家需要帮助的时候才出手干预(程序执行的过程中抛出异常的时候,😂),过滤器的处理异常的顺序是由小范围到大范围,即方法过滤器到控制器过滤器到全局过滤器

中间件是整个NestJS请求处理的最前沿,在中间件中可以修改请求对象或中断请求,中间件如果不调用next方法,请求将会被挂起,用户无法得到响应。

拦截器、守卫、管道则围绕着控制器和方法在做文章了。

当请求通过了中间件以后,首先进入守卫的逻辑,守卫的执行顺序是由大范围到小范围,即全局守卫到控制器守卫再到方法守卫,守卫中如果有一个返回false,后续的守卫将不再执行,请求也不再继续进行下去了,因此,守卫可以用来进行鉴权操作,守卫的定位是一个可以中断请求的拦截器

在请求被守卫放行以后,开始进行管道的操作,因为这个时候,请求参数(Request对象,Response对象,Next方法)被NestJS的路由处理成我们编写的控制器方法上所需要的参数,管道就在这个时机对参数进行转换。管道的执行顺序跟守卫的顺序是一样的,但是管道要比守卫多一类参数管道(Query、Param、Body这类路由参数管道),也是全局管道到控制器管道再到方法管道最后是参数管道

当管道将数据验证和转化的工作完成以后,就可以正式的进入到拦截器的逻辑了(看拦截器的拦截时机,有的在方法执行之前的处理的话,肯定得等方法完成执行,各位读者能够明白我的意思即可),拦截器是AOP在NestJS的运行时(这个运行时怎么解释呢,我是这样理解的。TS的装饰器是对代码的能力增强或削弱,而拦截器是对方法的内容进行处理)的体现。拦截器的执行顺序也是从全局拦截器到控制器拦截器最后到方法拦截器

不管是中间件、拦截器、守卫、管道,他们都是被包裹在过滤器的作用域内执行的(中间件的处理方式稍微跟其它不一样,但仍然是被其包裹的),这些控件在执行的过程中一旦抛出错误,将会进入过滤器的逻辑

记录我的NestJS探究历程(九)——管道

结语

如果您已经学完了我之前的文章的话,相信您已经熟练应用NestJS开发项目了,致敬努力向上的您。

我基本上为大家分析了一些NestJS的基础的实现,但是这个系列并没有完结,在后面的文章中,向大家分析一些更加高级的用法和编程技巧。

不过,这也意味着后文可能没有什么既定的顺序了,感谢各位读者的支持。