async-validator 是一个表单异步校验库,npm上每周下载量达120万次左右,代码设计十分优秀,这期我准备来详情解读一下源码,从中学习和了解作者的设计思想,便于在项目中更灵活的运用。
在小程序开发中用到了 uview
的 form
表单组件,在开发的过程中总是遇到各种验证问题,如配置的规则不生效、重置表单不刷新验证状态、设置自定义的验证规则、动态设置验证规则不生效等问题。所以简单了看了一下 uview
的 form
表单组件的内部实现,发现代码量很少,实现的非常简洁,之所以能简单实现,主要还把表单的验证逻辑独立出来了入到 async-validator.js
中,虽然没有引用 npm
上的 async-validator
,是因为有一些个性化的定制,所以放到 libs/util
目录下单独维护,但其核心实现就是 async-validator
,所以我们可以先解析 npm
上应用最为广泛的 async-validator
模块的源码,再回过头来看 uview
组件库中的改动。
截止到现在,async-validator 的版本为 4.2.5
, 每周下载量约120万左右,我们就看这个版本的源码内容
功能特点
- 可以指定类型(有内置15种类型)
- 可以自定义同步的
validator
和 异步的asyncValidator
- 可以自定义
message
,message
可以是函数,可以配置message
默认值用于国际化 - 可以指定正则表达式
- 可以配置转换器,在校验前转换数值
- 可以定义嵌套规则
- 可以设置校验方式,如同步校验所有规则,还是顺序校验有错时停止校验
- 字符串、数组、数值可以设置
len
min
max
校验长度和范围
使用示例
import Schema, {
Rules,
ValidateCallback,
ValidateFieldsError,
Values,
} from 'async-validator';
const rules:Rules = {
// 指定类型 number
age: {
required: true,
type: 'number',
},
// 自定义validator
name: {
type: 'string',
required: true,
// 返回 boolean
validator: (rule, value) => value === 'muji',
},
// 自定义 异步的validator
height: {
type: 'number',
// 返回 promise
asyncValidator: (rule, value) => {
return new Promise<void>((resolve, reject) => {
if (value < 18) {
reject('too young'); // reject with error message
} else {
resolve();
}
});
},
},
// 使用内置类型校验,添加多个验证规则
// 内置类型:string/number/boolean/method/regexp/integer/float/array/object/enum/date/url/hex/email/any
email: [
{
type: 'email',
required: true,
},
{
validator(rule, value, callback, source, options) {
const errors = [];
// 测试email地址是否已经在数组库中存在
// 并当已存在时在errors数组中添加一个error
return errors;
},
},
],
// 使用正则校验,并在校验前 转换原数值
subname: {
type: 'string',
required: true,
pattern: /^[a-z]+$/,
transform(value) {
return value.trim();
},
},
// 自定义 messaage, 可以是一个函数
firstname: {
type: 'string',
required: true,
message: () => 'name is required'
},
// 可以校验一个枚举值
role: {
type: 'enum',
enum: ['admin', 'user', 'guest']
},
// 可以设置嵌套校验规则
address: {
type: 'object',
required: true,
options: { first: true },
fields: {
street: { type: 'string', required: true },
city: { type: 'string', required: true },
zip: { type: 'string', required: true, len: 8, message: 'invalid zip' },
},
},
// 可用于校验 radio 或 checkbox的选中数值
isVip: {
type: 'enum',
enum: [0, 1],
}
}
const schema = new Schema(rules);
const source = {
age: '',
name: '',
height: 0,
email: '',
subname: '',
firstname: '',
role: '',
address: {},
isVip: 1
}
// 回调方式
schema.validate(source, (errors, fields) => {
if(errors){
console.log(errors, fields);
return;
}
console.log(fields)
});
// Promise 方式
schema.validate(source).then((source) => {
console.log('success')
}).catch(({errors, fields}) => {
console.log(errors, fields);
})
返回的错误和字段信息
// errors
[
{ message: 'age is required', fieldValue: '', field: 'age' },
{ message: 'name fails', fieldValue: '', field: 'name' },
{ message: 'email is required', fieldValue: '', field: 'email' },
{ message: 'subname is required', fieldValue: '', field: 'subname' },
{ message: 'name is required', fieldValue: '', field: 'firstname' },
{
message: 'role must be one of admin, user, guest',
fieldValue: '',
field: 'role'
},
{
message: 'address.street is required',
fieldValue: undefined,
field: 'address.street'
},
{ message: 'too young', fieldValue: 0, field: 'height' }
]
// fields
{
age: [ { message: 'age is required', fieldValue: '', field: 'age' } ],
name: [ { message: 'name fails', fieldValue: '', field: 'name' } ],
email: [ { message: 'email is required', fieldValue: '', field: 'email' } ],
subname: [
{
message: 'subname is required',
fieldValue: '',
field: 'subname'
}
],
firstname: [
{ message: 'name is required', fieldValue: '', field: 'firstname' }
],
role: [
{
message: 'role must be one of admin, user, guest',
fieldValue: '',
field: 'role'
}
],
'address.street': [
{
message: 'address.street is required',
fieldValue: undefined,
field: 'address.street'
}
],
height: [ { message: 'too young', fieldValue: 0, field: 'height' } ]
}
rule 的可配置选项
src/interface.ts
export interface RuleItem {
// 类型
type?: RuleType; // default type is 'string'
// 是否必填
required?: boolean;
// 正则
pattern?: RegExp | string;
// 最小值
min?: number; // Range of type 'string' and 'array'
// 最大值
max?: number; // Range of type 'string' and 'array'
// 长度
len?: number; // Length of type 'string' and 'array'
// 枚举值,值必是枚举值中的一种
enum?: Array<string | number | boolean | null | undefined>; // possible values of type 'enum'
// 是否校验空白符
whitespace?: boolean;
// 嵌套规则定义,如 { adress: { street, city, zip}}
fields?: Record<string, Rule>; // ignore when without required
// 额外的配置 {}
options?: ValidateOption;
// 用于统一定义对象或数组中每个元素的校验规则
defaultField?: Rule; // 'object' or 'array' containing validation rules
// 转换器,用于在校验前转换数据
transform?: (value: Value) => Value;
// 校验失败的消息
message?: string | ((a?: string) => string);
// 自定义的异步检验器
asyncValidator?: (
rule: InternalRuleItem,
value: Value,
callback: (error?: string | Error) => void,
source: Values,
options: ValidateOption,
) => void | Promise<void>;
// 自定义的同步校验器
validator?: (
rule: InternalRuleItem,
value: Value,
callback: (error?: string | Error) => void,
source: Values,
options: ValidateOption,
) => SyncValidateResult | void;
}
特殊的几个配置项
whitespace
一般如果字符串全部是空格的必填字段会视为错误,如果希望字符串是空格但希望校验通过,那么需要设置为 true
。
fields & defaultField
这两个配置都用于 object
和 array
的嵌套校验 fields
用于设置每一个字段或元素的校验规则 defaultField
用于设置全部字段或元素的校验规则
address: {
type: 'object',
required: true,
options: { first: true },
fields: {
street: { type: 'string', required: true },
city: { type: 'string', required: true },
zip: { type: 'string', required: true, len: 8, message: 'invalid zip' },
},
},
roles: {
type: 'array',
required: true,
len: 3,
fields: {
0: { type: 'string', required: true },
1: { type: 'string', required: true },
2: { type: 'string', required: true },
},
},
urls: {
type: 'array',
required: true,
defaultField: {
type: 'url'
},
},
transform
用于在校验前转换数据或格式化,比如去除字符两端的空白符
name: {
type: 'string',
required: true,
pattern: /^[a-z]+$/,
transform(value) {
return value.trim();
},
},
options
suppressWarning
:boolean
,是否禁止显示有关无效值的内部警告。first
:boolean
,当第一个验证规则生成错误时调用,不再处理任何验证规则。如果验证涉及多个异步调用(例如,数据库查询),并且只需要第一个错误,请使用此选项。firstFields
:boolean|String[]
,当指定字段的第一个验证规则生成错误时调用,不再处理同一字段的验证规则。true
表示所有字段都应用这个规则(只要遇到规则未通过,就不继续校验该字段后面的规则)。
默认的 message
配置
src/messages.ts
{
default: 'Validation error on field %s',
required: '%s is required',
enum: '%s must be one of %s',
whitespace: '%s cannot be empty',
date: {
format: '%s date %s is invalid for format %s',
parse: '%s date could not be parsed, %s is invalid ',
invalid: '%s date %s is invalid',
},
types: {
string: '%s is not a %s',
method: '%s is not a %s (function)',
array: '%s is not an %s',
object: '%s is not an %s',
number: '%s is not a %s',
date: '%s is not a %s',
boolean: '%s is not a %s',
integer: '%s is not an %s',
float: '%s is not a %s',
regexp: '%s is not a valid %s',
email: '%s is not a valid %s',
url: '%s is not a valid %s',
hex: '%s is not a valid %s',
},
string: {
len: '%s must be exactly %s characters',
min: '%s must be at least %s characters',
max: '%s cannot be longer than %s characters',
range: '%s must be between %s and %s characters',
},
number: {
len: '%s must equal %s',
min: '%s cannot be less than %s',
max: '%s cannot be greater than %s',
range: '%s must be between %s and %s',
},
array: {
len: '%s must be exactly %s in length',
min: '%s cannot be less than %s in length',
max: '%s cannot be greater than %s in length',
range: '%s must be between %s and %s in length',
},
pattern: {
mismatch: '%s value %s does not match pattern %s',
},
}
自定义 validator
的返回值
asyncValidator
异步校验器,必须返回一个promise
对象,如果是reject
,可以传递一个Error
对象validator
同步校验器,可以返回true
或false
, 也可以返回一个Error
对象,callback
也可以传递一个Error
对象和直接返回一个Error
对象效果相同,如果返回flase
,会使用自定义的message
,如果没有,则使用内置的message
v2: [
{
asyncValidator(rule, value) {
return Promise.reject(new Error('e3'));
},
},
],
v3: [
{
validator(rule, value, callback) {
callback(new Error('e1'));
},
},
{
validator() {
return new Error('e5');
},
},
{
validator() {
return false;
},
message: 'e6',
},
{
validator() {
return true;
},
},
],
总结
async-validator
经过几年的迭代,可以看出来是一个非常强大且成熟的验证器,几乎满足99%的验证场景,再结合组件库封装添加一些个性化UI的功能,就已经是非常完美了,实事也是如此,在看了antd、elementUI、iView等组件库表单验证的实现,无一不是使用了 async-validator
,在日常开发中,表单验证的业务场景太多,有必要深入了解一下。