核心流程
asyncMap
方法设计的较为复杂,核心是外层遍历字段,内层遍历字段的规则,外层的 next
函数用于计算字段校验进度,顺序校验器和并行校验器内部的 count
或 next
函数用于迭代字段的规则执行行 singleValidator
校验.
数据流转
构造器 new Schema
构造函数代码很简单,就是调用了 this.define(rules)
生成了 this.rules
,为了规范化 field
的规则为一个数组
define(rules: Rules) {
// 必须传递一个rules对象
if (!rules) {
throw new Error('Cannot configure a schema with no rules');
}
// 必须是 object
if (typeof rules !== 'object' || Array.isArray(rules)) {
throw new Error('Rules must be an object');
}
this.rules = {};
// 确保字段配置的规则为一个数组
Object.keys(rules).forEach(name => {
const item: Rule = rules[name];
// 不是数组的时候,包装为数组
this.rules[name] = Array.isArray(item) ? item : [item];
});
}
验证 validator
validate(source, options, calback){
// 1. (略)如果只有两个参数,则按 (source, callbck) 处理
// 2. (略)如果有自定义的全局 message, 则与默认的合并
// 3. 定义 complete 验证完成后的回调,用于处理并返回最终的异常
function complete(results) {
// 将 results 拍平加到 errors 数组中
let errors = [];
// 将 errors 数组转换成 以 field 为key的对象
let fields = {}
callback(errors, fields)
}
// 4. 遍历 rules 生成 series
const series = {};
Object.keys(rules).forEach((field) => {
rules[field].forEach(rule => {
series[field].push({
rule,
value,
source,
field
})
})
})
return asyncMap(
series,
options,
// 单个字段的验证
sigleValidator,
// 全部完成后的回调
results => {
complete(results);
},
source
)
}
sigleValidator
用来验证每一个规则
data
为series
中的每个元素{ rule, value, source, field}
doIt
为内部同步或顺序迭代器传递的count
或next
函数,用于判断是否需要继续校验
(data, doIt) => {
const rule = data.rule;
// 是否有嵌套规则
let deep =
(rule.type === 'object' || rule.type === 'array') &&
(typeof rule.fields === 'object' ||
typeof rule.defaultField === 'object');
// 有嵌套时,如果是必填 或者非必填但有值 才需要验证
deep = deep && (rule.required || (!rule.required && data.value));
rule.field = data.field;
// 用于生成嵌套字段的全路径 如 a.b.c
function addFullField(key: string, schema: RuleItem) {
return {
...schema,
fullField: `${rule.fullField}.${key}`,
fullFields: rule.fullFields ? [...rule.fullFields, key] : [key],
};
}
// cb: 生成异常信息对象,并调用内部的 next 或 count 函数来判断是否需要进行下一个校验
function cb(e: SyncErrorType | SyncErrorType[] = []) {
let errorList = Array.isArray(e) ? e : [e];
// 打印错误到控制台
if (!options.suppressWarning && errorList.length) {
Schema.warning('async-validator:', errorList);
}
// 如果配置了自定义的 message,就加到 errorList 中
if (errorList.length && rule.message !== undefined) {
errorList = [].concat(rule.message);
}
// 格式化 message,并添加额外的字段信息,返回 [{message, fieldValue, field}]
// Fill error info
let filledErrors = errorList.map(complementError(rule, source));
// 如果设置了规则:出错就停止 就回调doIt
if (options.first && filledErrors.length) {
// 这里设置了一下标志位 {age: 1} 未知其意
errorFields[rule.field] = 1;
return doIt(filledErrors);
}
// 如果没有嵌套字段校验,就回调doIt
if (!deep) {
doIt(filledErrors);
// 嵌套校验
} else {
// 如果是必填,但没有值,生成必填错误,回调 doIt
if (rule.required && !data.value) {
if (rule.message !== undefined) {
filledErrors = []
.concat(rule.message)
.map(complementError(rule, source));
} else if (options.error) {
filledErrors = [
options.error(
rule,
format(options.messages.required, rule.field),
),
];
}
return doIt(filledErrors);
}
// 需要嵌套校验的情况 ----------------- S
let fieldsSchema: Record<string, Rule> = {};
// 如果有 defaultField,那么给每个字段都加上 defaultField 规则
if (rule.defaultField) {
Object.keys(data.value).map(key => {
fieldsSchema[key] = rule.defaultField;
});
}
// 生成 field 的规则对象
fieldsSchema = {
...fieldsSchema,
...data.rule.fields,
};
const paredFieldsSchema: Record<string, RuleItem[]> = {};
// 添加 fullField 生成字段的全路径 a.b.c
Object.keys(fieldsSchema).forEach(field => {
const fieldSchema = fieldsSchema[field];
const fieldSchemaList = Array.isArray(fieldSchema)
? fieldSchema
: [fieldSchema];
paredFieldsSchema[field] = fieldSchemaList.map(
addFullField.bind(null, field),
);
});
const schema = new Schema(paredFieldsSchema);
// 向下传递父级的 message
schema.messages(options.messages);
// 向下传递父级的配置
if (data.rule.options) {
data.rule.options.messages = options.messages;
data.rule.options.error = options.error;
}
// 继续验证子级,相当于递归
schema.validate(data.value, data.rule.options || options, errs => {
const finalErrors = [];
if (filledErrors && filledErrors.length) {
finalErrors.push(...filledErrors);
}
if (errs && errs.length) {
finalErrors.push(...errs);
}
// 子级验证完成后,把错误集合传递给父级,向上传递
doIt(finalErrors.length ? finalErrors : null);
});
// 需要嵌套校验的情况 ----------------- E
}
}
// 执行 同步或异步的 validator 方法,获取结果
// 结果类型:Boolean、Array、Error、Promise
// 获取结果后,执行cb,进行下一步的规则校验
let res: ValidateResult;
// 异步validator
if (rule.asyncValidator) {
res = rule.asyncValidator(rule, data.value, cb, data.source, options);
// 同步的validator
} else if (rule.validator) {
try {
res = rule.validator(rule, data.value, cb, data.source, options);
} catch (error) {
console.error?.(error);
// rethrow to report error
if (!options.suppressValidatorError) {
setTimeout(() => {
throw error;
}, 0);
}
// 如果验证器执行报错,则把错误收集起来
cb(error.message);
}
// 对返回值做处理 boolean/array/Error
if (res === true) {
cb();
} else if (res === false) {
cb(
typeof rule.message === 'function'
? rule.message(rule.fullField || rule.field)
: rule.message || `${rule.fullField || rule.field} fails`,
);
} else if (res instanceof Array) {
cb(res);
} else if (res instanceof Error) {
cb(res.message);
}
}
// 如果返回的结果是 promise
if (res && (res as Promise<void>).then) {
(res as Promise<void>).then(
() => cb(),
e => cb(e),
);
}
},
asyncMap
asyncMap
函数会遍历 series
,遍历每个字段和字段的规则进行校验,返回一个 promise
,并调用 callback
返回最终结果
伪代码说明核心逻辑
series
主要有两层遍历,第一层是字段遍历,第二层是规则遍历,内层的规则遍历完后,会通过 next
函数将结果给外层并收集到 results
中,此时 total
加1,当所有字段遍历完后,调用 completeCallback
,返回最终结果
next
函数的功能是收集所有错误,并判断字段是否全部校验完成,返回最终结果
function asyncMap(series, options, singleValidator, completeCallback, source){
// 最终结果
let results = [];
let total = 0;
return new Promise((resolve, reject) => {
const next = errors => {
// 将 errors 添加到 results,并保证是一维数组
results.push.apply(results, errors);
// 验证完一个字段就加1
total++;
if(total === series.length){
// 所有字段验证完就 callback
completeCallback(results);
// 并返回promise
results.length ? reject(results) : resolve(source)
}
}
// 遍历 series
Object.keys(series).forEach(key => {
// 如果配置了 options.firstFields
if(options.firstFields){
// {age: [rule1, rule2]}
// 顺序校验规则 ,有错就终止当前字段的后面的规则校验
asyncSerialArray(series[key], singleValidator, next);
}else{
// 并行校验规则,所有规则都校验,完成后一次性返回
asyncParallelArray(series[key], singleValidator, next);
}
})
})
}
下面是源代码详情注释
export function asyncMap(
objArr: Record<string, RuleValuePackage[]>,
option: ValidateOption,
func: ValidateFunc,
callback: (errors: ValidateError[]) => void,
source: Values,
): Promise<Values> {
// 遇到异常就中止后面字段的校验(用于嵌套校验)
if (option.first) {
const pending = new Promise<Values>((resolve, reject) => {
const next = (errors: ValidateError[]) => {
// 结束验证,callback执行,并返回promise对象,reject 或 resolve;
callback(errors);
return errors.length
? reject(new AsyncValidationError(errors, convertFieldsError(errors)))
: resolve(source);
};
// 对象转数组
const flattenArr = flattenObjArr(objArr);
// 顺序校验,有错就返回错误,结束校验
asyncSerialArray(flattenArr, func, next);
});
pending.catch(e => e);
// 返回 promise 对象
return pending;
}
// 当指定字段的第一个校验规则产生error时调用callback
// 为true,只要遇到规则未通过,就继续校验后面规则,继续下一个字段的校验
// 默认为false, 也就是 firstFields = [] => 并行校验
const firstFields =
option.firstFields === true
? Object.keys(objArr)
: option.firstFields || [];
const objArrKeys = Object.keys(objArr);
const objArrLength = objArrKeys.length;
let total = 0;
const results: ValidateError[] = [];
const pending = new Promise<Values>((resolve, reject) => {
const next = (errors: ValidateError[]) => {
// 收集 errors, apply 用于拍平为一维数组
results.push.apply(results, errors);
// 一个字段的所有规则校验完成后,加1,计数字段完成数量
total++;
// 所有字段都校验完成后,调用 callback -> 结束校验
if (total === objArrLength) {
// 调用callback 返回结果
callback(results);
// 同时,返回一个Promise,便于外面使用then或catch调用
return results.length
? reject(
new AsyncValidationError(results, convertFieldsError(results)),
)
: resolve(source);
}
};
if (!objArrKeys.length) {
callback(results);
resolve(source);
}
// 遍历每个字段和字段的规则进行校验
objArrKeys.forEach(key => {
const arr = objArr[key];
// 如果设置了 某些字段规则校验是 顺序校验
if (firstFields.indexOf(key) !== -1) {
// 顺序校验规则,有错就终止
asyncSerialArray(arr, func, next);
} else {
// 并行校验规则,规则遍历完成后一次性返回
asyncParallelArray(arr, func, next);
}
});
});
pending.catch(e => e);
// 返回最终的 promise 对象
return pending;
}
规则校验的两种模式
asyncParallelArray
并行校验规则,所有规则都校验完成后就回调 callback
进行下一个字段的校验
function asyncParallelArray(
arr: RuleValuePackage[],
func: ValidateFunc,
callback: (errors: ValidateError[]) => void,
) {
const results: ValidateError[] = [];
let total = 0;
const arrLength = arr.length;
function count(errors: ValidateError[]) {
results.push(...(errors || []));
// 在校验完字段的 1个规则后,加1,计数规则校验数量
total++;
// 并行校验所有规则后,回调进行下一个字段的校验 callback -> 父next
if (total === arrLength) {
callback(results);
}
}
// 所有规则并行执行 func (也就是 asyncMap 的检验函数 (data, doIt) => void)
arr.forEach(a => {
func(a, count);
});
}
asyncSerialArray
顺序校验规则,有错就中断后面的规则校验,返回结果,回调 callback
进行下一个字段的校验
function asyncSerialArray(
arr: RuleValuePackage[],
func: ValidateFunc,
callback: (errors: ValidateError[]) => void,
) {
let index = 0;
const arrLength = arr.length;
function next(errors: ValidateError[]) {
// 如果有错就回调,不再继续下一个规则,调用callback(为pending中的next)继续下一个字段
if (errors && errors.length) {
callback(errors);
return;
}
// 没有错,就继续下一个规则
const original = index;
index = index + 1;
// 顺序执行 func(也就是 asyncMap 的检验函数)
if (original < arrLength) {
func(arr[original], next);
} else {
callback([]);
}
}
next([]);
}