Nestjs-Validation实践

396 阅读4分钟
前沿

在维护项目的时候,可能会遇到这样一种情况,新旧接口的参数名称不一致,或命名规范、大小写不统一。如果想统一规范接口,并映射到新的Nestjs API中的标准入参模型,并且不希望兼容匹配代码耦合到新的API标准中,如何才能干净利落的实现呢?

场景

我们有一个登陆接口,它的参数校验规格如下:

// 用户名和密码均不可为空
export class LoginDto {
    @IsNotEmpty({ message: '用户名不能为空' })
    username: string;
    @IsNotEmpty({ message: '密码不能为空' })
    password: string;
}

接口如下:

@Post('login')
login(@Body() body: LoginDto): any {
  // 简单返回收到的消息体
  return body
}

这样,前端调用:

POST http://test.com/login

body: {
    username: 'admin',
    password: 'admin'
}

就可以正常返回:

{
    username: 'admin',
    password: 'admin'
}

如果对Nestjs中如何配置ValidationPipe进行参数校验还不清楚,可以参看我这篇文章,很简单:Nest-Validation参数校验器

假设旧的标准是前端传入这样:

{
    userName: 'admin',
    pwd: 'admin'
}

该如何兼容呢?

实践

我们了解了ValidationPipe管道的用法,同样的,我们可以自定义一个转换参数的管道,应用到我们的接口上。首先,创建自定义管道。

每个管道都必须实现PipeTransform,并且实现transform()方法。transform方法有两个参数:

  • value:当前接口请求方法的参数
  • metadata:当前接口请求方法的参数元数据

metadata的接口定义是这样的:

export interface ArgumentMetadata {
  type: 'body' | 'query' | 'param' | 'custom'; // 接口参数来自于哪里
  metatype?: Type<unknown>; // 在接口处理方法里设置的参数类型声明,本文章示例中就是login接口中为body声明的LoginDto类型
  data?: string; // 传递给参数装饰器的值,例如@Body('name'),没传就是undefined
}

接下来,创建我们的自定义管道RequestConver.pipe.ts

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

@Injectable()
export class RequestConverPipe implements PipeTransform {
    // converParams是定义要转换的参数
    // 是一个数组,两个一组,一组包含旧参数和新参数
    // 本例子中就是这样:['userName','username','pwd','password'],按这样的规则解析
    constructor(private readonly converParams?: string[]) { }
    transform(value: any, metadata: ArgumentMetadata) {
        console.log('Run RequestConverPipe...', this.converParams, value, metadata)
      	// 一些转换代码
        return value;
    }
}

然后将此管道应用到我们的路由处理程序上:

@UsePipes(new RequestConverPipe(['userName','username','pwd','password']))
@Post('login2')
login2(@Body() body: LoginDto): any {
  return body
}

走到这里,我们自定义管道的设置和应用就可以了。

但是仔细想一想,我们的系统中的ValidationPipe管道是全局设置的,此处的自定义管道RequestConverPipe应用是局部的,所以在执行顺序上,ValidationPipe会先执行,所以对于login接口,旧的传参方法一样会被拦截掉。那ValidationPipe不全局设置不就可以了,但是这样就要为系统内所有需要参数验证的接口设置ValidationPipe,得不偿失,并不是好办法。

既然这样,只能在参数校验规则定义也就是dto文件里想办法了。

Nestjs提供了一些比较实用的函数来转换类型

  • PartialType():返回一个类型(类),其中输入类型的所有属性都设置为可选。
  • PickType():通过从输入类型中选择一组属性来构造一个新类型(类)。
  • OmitType():通过从输入类型中选取所有属性,然后删除一组特定的键来构造一个类型。
  • IntersectionType():将两个类型合并为一个新类型(类)

我们可以修改参数校验规则,如下:

import { IsNotEmpty, IsString } from 'class-validator';
import { PartialType, IntersectionType, OmitType } from '@nestjs/mapped-types';

export class LoginDto {
    @IsNotEmpty({ message: '用户名不能为空' })
    username: string;
    @IsNotEmpty({ message: '密码不能为空' })
    password: string;
}

class OldLoginDto {
    @IsNotEmpty({ message: '用户名不能为空' })
    userName: string;
    @IsNotEmpty({ message: '密码不能为空' })
    pwd: string;
}
export class NewLoginDto extends IntersectionType(
    PartialType(LoginDto),
    PartialType(OldLoginDto),
)

这样,把需要兼容的参数转换成可选,就可以绕过全局参数校验器的校验,然后在RequestConverPipe管道中去做校验和转换工作,就可以实现我们的目标。以最小的代价实现新旧接口的兼容,并在一定程度上做到新旧系统隔离。

当然,RequestConverPipe的验证转换逻辑就需要我们自己去写,这是不可避免的。

总结

以上是我了解了Validation后,对Nestjs提供的一些功能,结合可能遇到的应用场景,做的一个小实践。并不完美,但也有一些心得,如果有大佬知道更好的解决办法,欢迎评论区探讨!