参数校验
我们经常要在方法调用时执行一些类型检查,参数转换的操作,Midway 提供了一种简单的能力来快速检查参数的类型
本模块自 v4.0.0
起替换 @midwayjs/validate
组件。
新版本提供了更灵活的验证器扩展机制,支持多种验证器(如 Joi、Zod 等)的无缝切换,并提供了更好的类型支持和性能优化。
相关信息:
描述 | |
---|---|
可用于标准项目 | ✅ |
可用于 Serverless | ✅ |
可用于一体化 | ✅ |
包含独立主框架 | ❌ |
包含独立日志 | ❌ |
背景
最常用参数校验的地方是控制器(Controller),同时你也可以在任意的 Class 中使用这个能力。
我们以控制器(Controller)中使用为例。
➜ my_midway_app tree
.
├── src
│ ├── controller
│ │ └── user.ts
│ ├── interface.ts
│ └── service
│ └── user.ts
├── test
├── package.json
└─ ─ tsconfig.json
普通情况下,我们从 body
上拿到所有 Post 结果,并进行一些校验。
// src/interface.ts
export interface User {
id: number;
firstName: string;
lastName: string;
age: number;
}
// src/controller/home.ts
import { Controller, Get, Provide } from '@midwayjs/core';
@Controller('/api/user')
export class HomeController {
@Post('/')
async updateUser(@Body() user: User) {
if (!user.id || typeof user.id !== 'number') {
throw new Error('id error');
}
if (user.age <= 30) {
throw new Error('age not match');
}
// xxx
}
}
如果每个方法都需要这么校验,会非常的繁琐。
针对这种情况,Midway 提供了 Validation 组件。配合 @Validate
和 @Rule
装饰器,用来 快速定义校验的规则,帮助用户 减少这些重复的代码。
下面的通用能力将以 joi 来举例。
安装依赖
你需要安装 validation 组件以及对应验证器。
## 安装 validation 组件
$ npm i @midwayjs/validation@4 --save
## 选择一个或多个验证器
$ npm i @midwayjs/validation-joi@4 --save
## 基础库
$ npm i joi --save
或者在 package.json
中增加如下依赖后,重新安装。
{
"dependencies": {
"@midwayjs/validation": "^4.0.0",
"@midwayjs/validation-joi": "^4.0.0",
"joi": "^17.13.3",
// ...
},
"devDependencies": {
// ...
}
}
开启组件
在 configuration.ts
中增加组件:
import { Configuration, App } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import * as validation from '@midwayjs/validation';
import { join } from 'path';
@Configuration({
imports: [
koa,
validation,
// ... 其他组件
],
importConfigs: [join(__dirname, './config')],
})
export class MainConfiguration {
@App()
app: koa.Application;
async onReady() {
// ...
}
}
在配置文件中设置验证器:
// src/config/config.default.ts
import * as joi from '@midwayjs/validation-joi';
export default {
// ...
validation: {
// 配置验证器
validators: {
joi,
},
// 设置默认验证器
defaultValidator: 'joi'
}
}
校验规则
通过 @Rule
装饰器,可以传递校验规则。
import { Rule } from '@midwayjs/validation';
import * as Joi from 'joi';
export class UserDTO {
@Rule(Joi.number().required())
id: number;
@Rule(Joi.string().required())
firstName: string;
@Rule(Joi.string().max(10))
lastName: string;
@Rule(Joi.number().max(60))
age: number;
}
校验参数
定义完类型之后,就可以直接在业务代码中使用了。
// src/controller/home.ts
import { Controller, Get, Provide, Body } from '@midwayjs/core';
import { UserDTO } from './dto/user';
@Controller('/api/user')
export class HomeController {
@Post('/')
async updateUser(@Body() user: UserDTO) {
// user.id
}
}
所有的校验代码都通通不见了,业务变的更纯粹了,当然,记得要把原来的 user interface 换成 Class。
一旦校验失败,浏览器或者控制台就会报出类似的错误。
ValidationError: "id" is required
同时,由于定义了 id
的类型,在拿到字符串的情况下,会自动将 id 变为数字。
async updateUser(@Body() user: UserDTO ) {
// typeof user.id === 'number'
}
如果需要对方法级别单独配置信息,可以使用 @Validate
装饰器,比如单独配置错误状态。
// src/controller/home.ts
import { Controller, Get, Provide } from '@midwayjs/core';
import { Validate } from '@midwayjs/validation';
import { UserDTO } from './dto/user';
@Controller('/api/user')
export class HomeController {
@Post('/')
@Validate({
errorStatus: 422,
})
async updateUser(@Body() user: UserDTO) {
// user.id
}
}
@Validate
装饰器可以传递多个配置项,比如 errorStatus
,locale
等。
配置项 | 类型 | 描述 |
---|---|---|
errorStatus | number | 当校验出错时,返回的 Http 状态码,在 http 场景生效,默认 422 |
locale | string | 校验出错文本的默认语言,默认为 en_US ,会根据 i18n 组件的规则切换 |
throwValidateError | boolean | 是否抛出校验错误,默认 true ,如果设置为 false ,则返回校验结果 |
defaultValidator | string | 设置默认使用的验证器 |
校验结果
校验结果是一个对象,包含了校验的状态,校验的错误,校验的值等信息。Midway 对不同的验证器的返回值做了封装,统一了返回值的格式。
整体结构如下:
interface ValidateResult {
/**
* 校验是否成功
*/
status: boolean;
/**
* 校验错误,如果有多个错误,会返回第一个错误
*/
error?: any;
/**
* 校验的所有错误
*/
errors?: any[];
/**
* 校验错误信息,如果有多个错误,会返回第一个错误的信息
*/
message?: string;
/**
* 校验的所有错误信息
*/
messages?: string[];
/**
* 校验额外信息
*/
extra?: any;
}
不同的验证器返回都已经处理成相同的结构。
通用场景校验
如果参数不是 DTO,可以使用 @Valid
装饰器进行校验,@Valid
装饰器可以直接传递一个验证规则。
使用 Joi:
import { Controller, Get, Query } from '@midwayjs/core';
import { Valid } from '@midwayjs/validation';
import * as Joi from 'joi';
@Controller('/api/user')
export class HomeController {
@Get('/')
async getUser(@Valid(Joi.number().required()) @Query('id') id: number) {
// ...
}
}
使用 Zod:
import { Controller, Get, Query } from '@midwayjs/core';
import { Valid } from '@midwayjs/validation';
import { z } from 'zod';
@Controller('/api/user')
export class HomeController {
@Get('/')
async getUser(@Valid(z.number().min(1)) @Query('id') id: number) {
// ...
}
}
在非 Web 场景下,没有 @Body
等 Web 类装饰器的情况下,也可以使用 @Valid
装饰器来进行校验。
import { Valid } from '@midwayjs/validation';
import { Provide } from '@midwayjs/core';
import { UserDTO } from './dto/user';
@Provide()
export class UserService {
async updateUser(@Valid() user: UserDTO) {
// ...
}
}
校验管道
如果你的参数是基础类型,比如 number
, string
, boolean
,则可以使用组件提供的管道进行校验。
默认的 Web 参数装饰器都可以在第二个参数传入管道。
比如:
import { ParseIntPipe } from '@midwayjs/validation';
import { Controller, Post, Body } from '@midwayjs/core';
@Controller('/api/user')
export class HomeController {
@Post('/update_age')
async updateAge(@Body('age', [ParseIntPipe]) age: number) {
// ...
}
}
ParseIntPipe
管道可以将字符串,数字数据转换为数字,这样从请求参数获取到的 age
字段则会通过管道的校验并转换为数字格式。
可以使用 的内置管道有:
ParseIntPipe
ParseFloatPipe
ParseBoolPipe
DefaultValuePipe
ParseIntPipe
用于将参数转为整形数字。
import { ParseIntPipe } from '@midwayjs/validation';
// ...
async update(@Body('age', [ParseIntPipe]) age: number) {
return age;
}
update({ age: '12'} ); => 12
update({ age: '12.2'} ); => Error
update({ age: 'abc'} ); => Error
ParseFloatPipe
用于将参数转为浮点型数字数字。
import { ParseFloatPipe } from '@midwayjs/validation';
// ...
async update(@Body('size', [ParseFloatPipe]) size: number) {
return size;
}
update({ size: '12.2'} ); => 12.2
update({ size: '12'} ); => 12
ParseBoolPipe
用于将参数转为布尔值。
import { ParseBoolPipe } from '@midwayjs/validation';
// ...
async update(@Body('isMale', [ParseBoolPipe]) isMale: boolean) {
return isMale;
}
update({ isMale: 'true'} ); => true
update({ isMale: '0'} ); => Error
DefaultValuePipe
用于设定默认值。
import { DefaultValuePipe } from '@midwayjs/validation';
// ...
async update(@Body('nickName', [new DefaultValuePipe('anonymous')]) nickName: string) {
return nickName;
}
update({ nickName: undefined} ); => 'anonymous'
自定义校验管道
如果默认的管道不满足需求,可以通过继承,快速实现一个自定义校验管道,组件已经提供了一个 ParsePipe
类用于快速编写。
import { Pipe } from '@midwayjs/core';
import { ParsePipe, RuleType } from '@midwayjs/validation';
@Pipe()
export class ParseCustomDataPipe extends ParsePipe {
getSchema() {
// ...
}
}
getSchema
方法用于返回一个校验规则。
比如 ParseIntPipe
的代码如下,管道执行时会自动获取这个 schema 进行校验,并在校验成功后将值返回。
我们依旧拿 joi
来举例。
import { Pipe } from '@midwayjs/core';
import { ParsePipe, RuleType } from '@midwayjs/validation';
import * as Joi from 'joi';
@Pipe()
export class ParseIntPipe extends ParsePipe {
getSchema() {
return Joi.number().integer().required();
}
}
校验规则
- 新版本中已经移除了
RuleType
的使用,可以直接使用对应的验证器写法。 @Rule
装饰器可以传递不同类型的验证器的规则。- 在
@Rule
装饰器中,使用getSchema
方法,需要使用箭头函数。
常见的 joi 校验写法
import * as Joi from 'joi';
Joi.number().required(); // 数字,必填
Joi.string().empty(''); // 字符串非必填
Joi.number().max(10).min(1); // 数字,最大值和最小值
Joi.number().greater(10).less(50); // 数字,大于 10,小于 50
Joi.string().max(10).min(5); // 字符串,长度最大 10,最小 5
Joi.string().length(20); // 字符串,长度 20
Joi.string().pattern(/^[abc]+$/); // 字符串,匹配正则格式
Joi.object().length(5); // 对象,key 数量等于 5
Joi.array().items(Joi.string()); // 数组,每个元素是字符串
Joi.array().max(10); // 数组,最大长度为 10
Joi.array().min(10); // 数组,最小长度为 10
Joi.array().length(10); // 数组,长度为 10
Joi.string().allow(''); // 非必填字段传入空字符串
export enum DeviceType {
iOS = 'ios',
Android = 'android',
}
Joi.string().valid(...Object.values(DeviceType)) // 根据枚举值校验
级联校验
Midway 支持每个校验的 Class 中的属性依旧是一个对象。
我们给 UserDTO
增加一个属性 school
,并且赋予一个 SchoolDTO
类型。
import { Rule, getSchema } from '@midwayjs/validation';
import * as Joi from 'joi';
export class SchoolDTO {
@Rule(Joi.string().required())
name: string;
@Rule(Joi.string())
address: string;
}
export class UserDTO {
@Rule(Joi.number().required())
id: number;
@Rule(Joi.string().required())
firstName: string;
@Rule(Joi.string().max(10))
lastName: string;
// 复杂对象
// 这里执行的时候 validator 还未注册,所以需要使用箭头函数
@Rule(() => getSchema(SchoolDTO).required())
school: SchoolDTO;
// 对象数组
@Rule(() => Joi.array().items(getSchema(SchoolDTO)).required())
schoolList: SchoolDTO[];
}
这个时候, @Rule
装饰器的参数可以为需要校验的这个类型本身。
继承校验
Midway 支持校验继承方式,满足开发者抽离通用的对象属性的时候做参数校验。
例如我们下面 CommonUserDTO
抽离接口的通用的一些属性,然后 UserDTO
作为特殊接口需要的特定参数。
import { Rule } from '@midwayjs/validation';
export class CommonUserDTO {
@Rule(Joi.string().required())
token: string;
@Rule(Joi.string())
workId: string;
}
export class UserDTO extends CommonUserDTO {
@Rule(Joi.string().required())
name: string;
}
老版本需要在子类上面加,新版本不需要啦~
如果属性名相同,则取当前属性的规则进行校验,不会和父类合并。
多类型校验
从 v3.4.5 开始,Midway 支持某个属性的不同类型的校验。
例如某个类型,既可以是可以普通类型,又可以是一个复杂类型。
import { Rule, getSchema } from '@midwayjs/validation';
import * as Joi from 'joi';
export class SchoolDTO {
@Rule(Joi.string().required())
name: string;
@Rule(Joi.string())
address: string;
}
export class UserDTO {
@Rule(Joi.string().required())
name: string;
@Rule(() => Joi.alternatives([Joi.string(), getSchema(SchoolDTO)]).required())
school: string | SchoolDTO;
}
我们可以使用 getSchema
方法,从某个 DTO 拿到当前的 schema,从而进行复杂的逻辑处理。
从原有 DTO 创建新 DTO
有时候,我们会希望从某个 DTO 中获取一部分属性,变成一个新的 DTO 类。
Midway 提供了 PickDto
和 OmitDto
两个方法根据现有的的 DTO 类型创建新的 DTO。
PickDto
用于从现有的 DTO 中获取一些属性,变成新的 DTO,而 OmitDto
用于将其中某些属性剔除,比如:
// src/dto/user.ts
import { Rule, PickDto } from '@midwayjs/validation';
export class UserDTO {
@Rule(Joi.number().required())
id: number;
@Rule(Joi.string().required())
firstName: string;
@Rule(Joi.string().max(10))
lastName: string;
@Rule(Joi.number().max(60))
age: number;
}
// 继承出一个新的 DTO
export class SimpleUserDTO extends PickDto(UserDTO, ['firstName', 'lastName']) {}
// const simpleUser = new SimpleUserDTO();
// 只包含了 firstName 和 lastName 属性
// simpleUser.firstName = xxx
export class NewUserDTO extends OmitDto(UserDTO, ['age']) {}
// const newUser = new NewUserDTO();
// newUser.age 定义和属性都不存在
// 使用
async login(@Body() user: NewUserDTO) {
// ...
}