Skip to content

表单验证

安装配置

向军大叔每晚八点在 抖音bilibli 直播

表单验证使用管道操作,所以你需要对PIPE管道有所了解。

首先创建验证需要的包 class-validatorclass-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 等工具对 UserControllercreate 方法行测试

你可以尝试不传递 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 支持classdecorator两种定义验证规则方式

表单匹配

表单匹配规则就是验证两个提交的表单值是否相同,比如验证密码与确认密码是否相同。

确认密码检验说明

  • 如果密码字段为 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 }));
	...
}