Appearance
表单验证
安装配置
表单验证使用管道操作,所以你需要对PIPE管道有所了解。
首先创建验证需要的包 class-validator 和 class-transformer
pnpm add class-validator class-transformer
pnpm add -D @nestjs/mapped-types
然后在 main.ts
中注册全局验证管道
async function bootstrap() {
...
app.useGlobalPipes(new ValidationPipe());
...
}
bootstrap();
验证实现
下面我们自己来写一个验证逻辑
首先创建 dto(Data Transfer Object) 文件 auth/dto/create-user.dto.s ,对请求数据进行验证规则声明。
$property 指当前表单字段
import { IsNotEmpty } from 'class-validator';
export class CreateUserDto {
@IsNotEmpty({message: '$property:用户名不能为空'})
name: string;
//存在price时才验证
@ValidateIf((o) => o.price)
//将类型转换为数值
@Type(() => Number)
price:number
}
然后创建 validate.pipe.ts 验证管道
import {
ArgumentMetadata,
BadRequestException,
Injectable,
PipeTransform,
} from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
@Injectable()
export class ValidatePipe implements PipeTransform {
async transform(value: any, metadata: ArgumentMetadata) {
const { metatype } = metadata;
//前台提交的表单数据没有类型,使用 plainToClass 转为有类型的对象用于验证
const object = plainToInstance(metatype, value);
//根据 DTO 中的装饰器进行验证
const errors = await validate(object);
if (errors.length) {
throw new BadRequestException('表单数据错误');
}
return value;
}
}
然后在控制器方法中使用验证管道进行验证
@Post('add')
add(@Body(ValidatePipe) dto: UserDto): any {
return dto;
}
内置验证
NestJs 提供了开箱即用的验证,不需要我们自己来实现验证,我们现在来体验
首先在 main.ts 全局注册验证管道
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Validate } from './validate';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
//注册验证管道
app.useGlobalPipes(new Validate());
await app.listen(3000);
}
bootstrap();
创建user资源用于进行验证实验
nest g res user
创建dto(Data Transfer Object) 文件 auth/dto/create-user.dto.ts ,用于定义验证规则
import { IsNotEmpty } from 'class-validator';
export class CreateUserDto {
@IsNotEmpty({message: '用户名不能为空'})
name: string;
}
在需要验证的控制器方法中使用 DTO
import { Body, Controller, Get, Post } from '@nestjs/common';
import { RegisterDto } from './dto/register.dto';
@Controller('user')
export class AuthController {
@Post()
register(@Body() dto: CreateUserDto) {
return dto;
}
}
然后使用 postman 等工具对 UserController
的 create
方法行测试
你可以尝试不传递 name参数 ,将报以下错误
{
"statusCode": 400,
"message": [
"用户名不能为空"
],
"error": "Bad Request"
}
类型映射
一般情况下更新与添加的Dto是类似的,这时可以使用 类型映射 优化代码,类型映射内部使用了 @nestjs/mapped-types 包。
下面是nest.js提供的常用类型映射函数。
类型映射 | 说明 |
---|---|
PickType | 函数通过挑出输入类型的一组属性构造一个新的类型(类) |
PartialType | 函数返回一个类型(一个类)包含被设置成可选的所有输入类型的属性 |
OmitType | 函数通过挑出输入类型中的全部属性,然后移除一组特定的属性构造一个类型 |
IntersectionType | 函数将两个类型合并成一个类型 |
下面是 UpdateArticleDto 继承 CreateArticleDto,并将所有属性设置为可选,更多使用请参考类型映射文档。
import { PartialType } from '@nestjs/mapped-types';
import { CreateArticleDto } from './create-article.dto';
export class UpdateArticleDto extends PartialType(CreateArticleDto) {}
下面是 UpdateArticleDto 继承 CreateArticleDto 但排除 createdAt 属性
import { OmitType } from '@nestjs/mapped-types'
import { RegisterDto } from './register.dto'
export class UpdateArticleDto extends OmitType(CreateArticleDto, ['createdAt']) {}
验证规则
斑马兽定义一些开发时常用的规则,因为验证使用 class-validator 所以要按其要求配置。
- 建议将验证规则统一保存在 src/common/rules 目录,并以 .rule.ts 结尾。
- NestJs 支持class与decorator两种定义验证规则方式
表单匹配
表单匹配规则就是验证两个提交的表单值是否相同,比如验证密码与确认密码是否相同。
确认密码检验说明
- 如果密码字段为 password 则确认密码字段必须使用 _confirmation 为后缀即 password_confirmation
- 如果 password_confirmation 没有定义在Dto中,需要将 ValidationPipe 的选项 whitelist: false ,否则验证装饰器得不到 password_confirmation 值
下面介绍类与装饰器两种定义方式
类方式定义
import { PrismaClient } from '@prisma/client';
import {
ValidationArguments,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
@ValidatorConstraint()
export class IsConfirmedRule implements ValidatorConstraintInterface {
async validate(value: string, args: ValidationArguments) {
return value == args.object[`${args.property}_confirmation`];
}
defaultMessage(args: ValidationArguments) {
return '数据不匹配';
}
}
在DTO中使用验证规则
import { IsNotEmpty,Validate } from 'class-validator'
import { IsConfirmedRule } from 'src/rules/is.confirmed.rule'
export class RegisterDto {
@IsNotEmpty()
@Validate(IsConfirmedRule,{ message: '两次密码不一致' })
password: string
}
装饰器定义
下面是验证装饰器 rules/is.confirm.pipe.ts 的内容
import {
registerDecorator,
ValidationArguments,
ValidationOptions,
} from 'class-validator';
//表字段是否唯一
export function IsConfirmedRule(validationOptions?: ValidationOptions) {
return function (object: Record<string, any>, propertyName: string) {
registerDecorator({
name: 'IsConfirmedRule',
target: object.constructor,
propertyName: propertyName,
constraints: [],
options: validationOptions,
validator: {
validate(value: string, args: ValidationArguments) {
return value == args.object[`${args.property}_confirmation`];
},
}
});
};
}
在DTO中使用验证规则
import { IsNotEmpty } from 'class-validator'
import { IsConfirmedRule } from 'src/rules/is.confirmed.rule'
export class RegisterDto {
@IsConfirmedRule({ message: '两次密码不一致' })
@IsNotEmpty()
password: string
}
表值不存在
数据表中不存在该值,就验证通过。比如用户注册时,注册邮箱就不能存在。
- 因为需要查表,所以validator 方法要定义为异步
下面是验证装饰器内容,文件位置是 src/common/rules/is-no-exists.validate.ts
import { PrismaClient } from '@prisma/client'
import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator'
//表字段是否唯一
export function IsNotExists(table: string, validationOptions?: ValidationOptions) {
return function (object: Record<string, any>, propertyName: string) {
registerDecorator({
name: 'IsNotExists',
target: object.constructor,
propertyName: propertyName,
constraints: [table],
options: validationOptions,
validator: {
async validate(value: string, args: ValidationArguments) {
const prisma = new PrismaClient()
const res = await prisma[table].findFirst({
where: {
[args.property]: value,
},
})
return !Boolean(res)
},
},
})
}
}
在DTO中使用验证规则
import { IsEmail, IsNotEmpty } from 'class-validator';
import { IsNotExists } from 'src/rules/unique.rule';
export class CreateAuthDto {
//使用自定义验证
@IsNotExists('users', { message: '用户已经存在' })
email: string;
}
表值存在
其实就是与上面规则含义相反,指值在数据表里存在就验证通过。
比如邮箱登录时,就要求该邮箱在数据表里已经存在
import { PrismaClient } from '@prisma/client'
import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator'
//表字段是否唯一
export function IsExists(property: { field: string; table: string }, validationOptions?: ValidationOptions) {
return function (object: Record<string, any>, propertyName: string) {
registerDecorator({
name: 'IsNotExists',
target: object.constructor,
propertyName: propertyName,
constraints: [table],
options: validationOptions,
validator: {
async validate(value: string, args: ValidationArguments) {
const prisma = new PrismaClient()
return await prisma[table].findFirst({
where: {
[args.property]: value,
},
})
},
},
})
}
}
自定义错误格式
我们可以对响应的消息进行自定义处理,方便前端 vue/react 的使用。
定义类
下面创建 src/filters/Validate.ts 验证类,用于扩展系统提供的 ValidationPipe 验证管道。
- 对响应的错误消息添加表单名称
import { HttpException, HttpStatus, ValidationError, ValidationPipe } from '@nestjs/common'
export default class ValidatePipe extends ValidationPipe {
protected flattenValidationErrors(validationErrors: ValidationError[]): string[] {
const messages = validationErrors.map((error) => {
return { field: error.property, message: Object.values(error.constraints)[0] }
})
throw new HttpException(
{
code: HttpStatus.BAD_REQUEST,
messages,
error: 'bad request',
},
HttpStatus.BAD_REQUEST,
)
}
}
声明验证
然后在 main.ts 中使用我们定义的验证类
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { ValidateExceptionFilter } from './common/exceptions/validate.exception'
import Validate from './common/rules/ValidatePipe'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.useGlobalPipes(new ValidatePipe())
await app.listen(3000)
}
bootstrap()
响应结果
如果验证失败时,将会有类似如下结果,方便前端识别是哪个表单产生了错误。
{
"code": 400,
"messages": [
{
"field": "name",
"message": "用户已经注册"
}
],
"error": "bad request"
}
其他配置
我们再来看一下其他影响验证的配置
自动转换
ValidationPipe 可以根据对象的 DTO 类自动将有效数据转换为对象类型。
如果不使用自动转换时,下面的id为string
@Get(':id')
index(@Param('id') id: number) {
console.log(typeof id);
}
在main.ts中设置全局自动转换后,上面的**id****类型自动转换为 number
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
}),
);
白名单
想过滤掉在 Dto 中没有声明的字段,可以在 main.ts 文件中对 ValidationPipe 管道进行配置。
async function bootstrap() {
...
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
...
}