背景
在使用ivew.design的时候,在源码中发现form表单的验证是使用Async Validator,然后就去看一下源码,了解原理并做一下整理。
const validator = new AsyncValidator(descriptor);
let model = {};
model[this.prop] = this.fieldValue;
validator.validate(model, { firstFields: true }, errors => {
this.validateState = !errors ? 'success' : 'error';
this.validateMessage = errors ? errors[0].message : '';
callback(this.validateMessage);
this.FormInstance && this.FormInstance.$emit('on-validate', this.prop, !errors, this.validateMessage || null);
});
这个是源码的版本
先了解咋用
- gitHub源码地址
star还是蛮多的,很多库都用了这个,只能说666~
安装
npm i async-validator
- 用法
更多的细节就从源码里面去说明了,说多了也记不得~
总体解析
- 大致设计如下
- 最后导出的Schema构造函数
由上面可以看出,这个js库最后导出Schema构造函数,并且分成几个大块,原型链上面的方法,register,warning,messages,validators
warning方法
在实例化 Schema 之前设置 warning 方法。该方法实际上是util.js文件里面的一个工具方法
如果放弃所有的控制太警告,你可以自己初始化
Schema.warning = () => {}
//默认是一个空函数,这样if条件没有通过时将不会在控制台打印警告
export let warning = () => {};
// 在生产环境或者node runtime时,不打印警告信息
if (
typeof process !== 'undefined' &&
process.env &&
process.env.NODE_ENV !== 'production' &&
typeof window !== 'undefined' &&
typeof document !== 'undefined'
) {
warning = (type, errors) => {
if (typeof console !== 'undefined' && console.warn) {
if (errors.every(e => typeof e === 'string')) {
//实际上就是一个console.warn
console.warn(type, errors);
}
}
};
}
messages方法
在 Schema
中的message方法实际上是message.js文件中导出的实例,是根据不同类型的失败校验的提示用的消息模板.
//index.js
//构造函数
function Schema(descriptor) {
//descriptor声明了校验规则
this.rules = null;
//使用私有属性_messages保存defaultMessages
this._messages = defaultMessages;
this.define(descriptor);
}
Schema.messages = defaultMessages;
//message.js
export function newMessages() {
return {
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',
},
clone() {
// 深拷贝
const cloned = JSON.parse(JSON.stringify(this));
cloned.clone = this.clone;
return cloned;
},
};
}
export const messages = newMessages();
当然有必要的话,这个messages的模板自己也是可以改造的。
//index.js
//Schema.prototype的原型方法中有message的方法
messages(messages) {
if (messages) {
//将 _messages和参数 深度合并合并
this._messages = deepMerge(newMessages(), messages);
}
return this._messages;
}
//util.js
//deepMerge是util.js中的一个普通合并对象的方法,先是遍历一遍对象,然后对下一层的对象使用结构,实际上只有2层的合并
export function deepMerge(target, source) {
if (source) {
for (const s in source) {
if (source.hasOwnProperty(s)) {
const value = source[s];
if (typeof value === 'object' && typeof target[s] === 'object') {
target[s] = {
...target[s],
...value,
};
} else {
target[s] = value;
}
}
}
}
return target;
}
//gitHub给到的例子
import Schema from 'async-validator';
const cn = {
required: '%s 必填',
};
const descriptor = { name: { type: 'string', required: true } };
const validator = new Schema(descriptor);
// 将cn与defaultMessages深层合并
validator.messages(cn);
...
validators
- 为用户提供的各种数据类型的验证方法
import validators from './validator/index';
Schema.validators = validators;
以对string类型的判断为例
- rule: 在源descriptor中,与要校验的字段名称相对应的校验规则。始终为它分配一个field属性,其中包含要验证的字段的名称。
//这个样子
{
[field: string]: RuleItem | RuleItem[]
}
//例子
{name:{type: "string", required: true, message: "Name is required"}}
- value: 源对象属性中要校验的值。
- callback: 校验完成后需要调用的callback。传递一个Error实例数组以判断校验失败。如果校验是同步的,则可以直接返回false、Error或Error Array
callback(errors)
- source: 传给validate 方法的源对象
- options: 额外选项
//options的内部属性
export interface ValidateOption {
// 是否取消关于无效值的内部警告
suppressWarning?: boolean;
// 当第一个验证规则生成错误时,停止处理
first?: boolean;
//当指定字段的第一个验证规则生成错误时,停止处理字段,“true”表示所有字段。
firstFields?: boolean | string[];
}
- options.messages: 包含校验 error message 的对象,将与 defaultMessages 进行深度合并
//所有的验证的方法都是以rule, value, callback, source, options为参数
function string(rule, value, callback, source, options) {
// 需要callback出去的错误列表
const errors = [];
//首先验证required为false或者还未填写状态就直接返回
const validate =
rule.required || (!rule.required && source.hasOwnProperty(rule.field));
if (validate) {
//isEmptyValue判断是否为空值,并且将空数组也判断为true
if (isEmptyValue(value, 'string') && !rule.required) {
return callback();
}
//使用rules方法判断是否必填
rules.required(rule, value, source, errors, options, 'string');
if (!isEmptyValue(value, 'string')) {
// 判断type,range范围(这里有涉及了len,min,max判断),以及提供正则表达式的判断
rules.type(rule, value, source, errors, options);
rules.range(rule, value, source, errors, options);
rules.pattern(rule, value, source, errors, options);
if (rule.whitespace === true) {
//为仅由空格组成的字符串添加额外的校验
rules.whitespace(rule, value, source, errors, options);
}
}
}
callback(errors);
}
export default string;
register
除了上述提供的validators的方法,这个register用于自定义判断
Schema.register = function register(type, validator) {
//必须是函数
if (typeof validator !== 'function') {
throw new Error(
'Cannot register a validator by type, validator is not a function',
);
}
validators[type] = validator;
};
为validators验证的方法提供的rule
/**
* 所有的方法的传值如下
* @param rule 校验的规则
* @param value source对象中该字段的值
* @param source 要校验的source对象
* @param errors 本次校验将要去添加的errors数组
* @param options 校验选项
* @param options.messages 校验的messages
*/
export default {
required, //属性为必填
whitespace, //校验空白字符
type, //判断type属性
range, //通过传入的number类型的len,min,max 进行判断
enum: enumRule,//校验值是否存在在枚举值列表中
pattern,//校验是否符合校验正则表达式
};
type有如下类型
const custom = [
'integer',
'float',
'array',
'regexp',
'object',
'method',
'email',
'number',
'date',
'url',
'hex',
];
// 枚举验证 官方案例
const descriptor = {
role: { type: 'enum', enum: ['admin', 'user', 'guest'] },
};
原型链上面的方法
- messages
//上文有交代,用于新增自己的错误消息提示模板
messages(messages) {
if (messages) {
this._messages = deepMerge(newMessages(), messages);
}
return this._messages;
},
- define
注册校验规则rules
- getType
通过此方法获得rule的type,并会判断此type是否是已经定义好的validators中的type
- getValidationMethod
通过此方法获得rule的validator
getValidationMethod(rule) {
//定义好了validator直接返回
if (typeof rule.validator === 'function') {
return rule.validator;
}
//收集单个rule里面定义的所有key
const keys = Object.keys(rule);
const messageIndex = keys.indexOf('message');
if (messageIndex !== -1) {
//删除message属性
keys.splice(messageIndex, 1);
}
//除了message,就只有一个key且是required,返回validators提供的required方法
if (keys.length === 1 && keys[0] === 'required') {
return validators.required;
}
//否则,最后一种情况,根据rule定义的type判断
// 如果type是非法未定义的type则直接返回false
return validators[this.getType(rule)] || false;
},
- validate
核心方法
1. 参数:
source_ 需要校验的对象
o 即options 描述校验的处理选项的对象(上文说的,suppressWarning,firstFields,first)
oc 即callback 校验完成后的回调函数
2. 返回值是一个Promise对象:
then 校验通过
catch({errors,fields}) 校验失败
errors是一个所有error数组,fiels是一个类似{field1:[error,error...],field2:[error,error...]}的对象
不得不说asyncMap又是怎么内容
首先它。若options.first为否值,;再根据options.firstFields是否为真值,分别执行asyncSerialArray、asyncParallelArray函数。
export function asyncMap(objArr, option, func, callback) {
//判断first
if (option.first) {
//判断options.first是否为真值,若为真值,调用asyncSerialArray处理series数组
//当某一规则校验失败时,即终止校验,执行callback回调
const pending = new Promise((resolve, reject) => {
const next = errors => {
callback(errors);
return errors.length
? reject(new AsyncValidationError(errors, convertFieldsError(errors)))
: resolve();
};
const flattenArr = flattenObjArr(objArr);
asyncSerialArray(flattenArr, func, next);
});
pending.catch(e => e);
return pending;
}
let firstFields = option.firstFields || [];
if (firstFields === true) {
firstFields = Object.keys(objArr);
}
const objArrKeys = Object.keys(objArr);
const objArrLength = objArrKeys.length;
let total = 0;
const results = [];
const pending = new Promise((resolve, reject) => {
//构建next函数包装callback,目的是将所有校验器的失败文案合并再传入callback
const next = errors => {
results.push.apply(results, errors);
total++;
if (total === objArrLength) {
callback(results);
return results.length
? reject(
new AsyncValidationError(results, convertFieldsError(results)),
)
: resolve();
}
};
if (!objArrKeys.length) {
callback(results);
resolve();
}
objArrKeys.forEach(key => {
const arr = objArr[key];
//判断firstFields值,分别执行asyncSerialArray、asyncParallelArray函数
//asyncParallelArray函数用于实现平行校验,在某个异步校验器执行过程中,平行调用下一个校验器;
//asyncSerialArray用于实现有序校验,在异步校验器执行完成后,再启用下一个校验器。
if (firstFields.indexOf(key) !== -1) {
asyncSerialArray(arr, func, next);
} else {
asyncParallelArray(arr, func, next);
}
});
});
pending.catch(e => e);
return pending;
}
最后
就这样了,有新的了解,后面再编辑,修改
喜欢,帮忙点赞?
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。