前沿
在维护项目的时候,可能会遇到这样一种情况,新旧接口的参数名称不一致,或命名规范、大小写不统一。如果想统一规范接口,并映射到新的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提供的一些功能,结合可能遇到的应用场景,做的一个小实践。并不完美,但也有一些心得,如果有大佬知道更好的解决办法,欢迎评论区探讨!