大家好,我是若川。持续组织了8个月源码共读活动,感兴趣的可以 点此加我微信ruochuan12 参与,每周大家一起学习200行左右的源码,共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列。另外:目前建有江西|湖南|湖北籍前端群,可加我微信进群。


平常开发写 element 表单的时候,肯定少不了表单的校验,element 使用的是 async-validator 这个开源库。

44ab91e5ead0f352cafea54c5d19d373.png
image-20220517090312952

这篇文章详细分析一下 async-validator 的主流程。

使用方法

import Schema from 'async-validator';
const descriptor = {
  list: {
    required: true,
    type: 'number',
  },
  limit: [
    {
      required: true,
      message: '数量必填',
    },
    {
      validator(r, v, cb) {
        if (v < 100) {
          return cb(new Error('数量不能小于 100'));
        }
        cb();
      },
    },
  ],
};
const validator = new Schema(descriptor);
validator.validate(
  { list: '12', limit: null },
  { firstFields: true },
  (errors, fields) => {
    if (errors) {
      console.log('错误列表', errors);
    }
  },
);

我们需要定义 descriptor ,也就是我们在 element 中定义的 rules ,然后创建一个 Schema 对象。

最后调用 validate 函数,传递三个参数:

第一个参数是要校验的对象

第二个参数是 options 对象, firstFieldstrue ,表示同一个字段如果有多个校验规则,一旦出现校验不通过的规则后边的规则就不执行了。

还可以设置 firsttrue,这个是针对整个校验对象的,如果某个字段校验不通过,那么后边所有的字段就不再校验了。

第三个参数是校验结束后的回调函数,erros 保存了所有校验失败的字段以及 message 信息。

因此,上边代码的输出如下:

58af70b427694d197be4032df2966133.png
image-20220519081546339

list 对应结果的 message 是默认为我们添加的,limit 对应结果的 message 是我们自己设置的,会覆盖默认的 message

因为我们设置了 firstFieldstrue ,所以只校验了 limit 的第一个规则,第二个规则就没有走到。

我们给 limit 设置一个值,让它走到第二个校验规则。

validator.validate(
  { list: '12', limit: 3 },
  { firstFields: true },
  (errors, fields) => {
    if (errors) {
      console.log('错误列表', errors);
    }
  },
);

输出如下:

6ead22d9ac6a7437d0f05238ed1731c1.png
image-20220519081840212

此时 limit 对应结果就是一个 Error 对象了,Error 对象除了本身的 message 属性,默认还为我们添加了 fieldfiledValue 属性。

预处理 descriptor

校验前  async-validator  会将传入的 descriptor 规范化。

我们传进入的是下边的样子:

const descriptor = {
  list: {
    required: true,
    type: 'number',
  },
  limit: [
    {
      required: true,
      message: '数量必填',
    },
    {
      validator(r, v, cb) {
        if (v < 100) {
          return cb(new Error('数量不能小于 100'));
        }
        cb();
      },
    },
  ],
};

预处理后会变成下边的样子:

{
  list: [
    {
      rule: {
        required: true,
        type: 'number',
        field: 'list',
        fullField: 'list',
        validator: (rule, value, callback, source, options) => {
          const errors = [];
          const validate =
            rule.required ||
            (!rule.required && source.hasOwnProperty(rule.field));
          if (validate) {
            if (value === '') {
              value = undefined;
            }
            if (isEmptyValue(value) && !rule.required) {
              return callback();
            }
            rules.required(rule, value, source, errors, options);
            if (value !== undefined) {
              rules.type(rule, value, source, errors, options);
              rules.range(rule, value, source, errors, options);
            }
          }
          callback(errors);
        },
      },
      value: '12',
      source: {
        list: '12',
        limit: 3,
      },
      field: 'list',
    },
  ],
  limit: [
    {
      rule: {
        required: true,
        message: '数量必填',
        field: 'limit',
        fullField: 'limit',
        type: 'string',
        validator: (rule, value, callback, source, options) => {
          const errors = [];
          const type = Array.isArray(value) ? 'array' : typeof value;
          rules.required(rule, value, source, errors, options, type);
          callback(errors);
        },
      },
      value: 3,
      source: {
        list: '12',
        limit: 3,
      },
      field: 'limit',
    },
    {
      rule: {
        field: 'limit',
        fullField: 'limit',
        type: 'string',
        validator(r, v, cb) {
          if (v < 100) {
            return cb(new Error('数量不能小于 100'));
          }
          cb();
        },
      },
      value: 3,
      source: {
        list: '12',
        limit: 3,
      },
      field: 'limit',
    },
  ],
};

主要做了三件事情:

  1. 把每个字段的校验规则统一成了一个数组对象

  2. 把原本的校验对象放到了 rule 属性中,并且添加了 valuesourcefield 属性

  3. 根据 requiredtype 补充了默认的 validator 校验函数

预处理 descriptor 对应的源码

让我们过一下这部分源码。

在构造函数中,把 descriptor 所有字段的 rule 转为了数组,保存到 rules 对象中。

constructor(descriptor: Rules) {
  this.define(descriptor);
}

define(rules: Rules) {
  if (!rules) {
    throw new Error('Cannot configure a schema with no rules');
  }
  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];
  });
}

剩下的处理都在 validate 函数中了,可以跟随下边的注释看一下:

validate(source_: Values, o: any = {}, oc: any = () => {}): Promise<Values> {
    let source: Values = source_;
    let options: ValidateOption = o;
    let callback: ValidateCallback = oc;
    if (typeof options === 'function') {
      callback = options;
      options = {};
    }
    ...

    function complete(results: (ValidateError | ValidateError[])[]) {
      ...
    }

    const series: Record<string, RuleValuePackage[]> = {};
    const keys = options.keys || Object.keys(this.rules); // 得到所有的要校验的 key
    keys.forEach(z => { // 遍历所有字段
      const arr = this.rules[z];
      let value = source[z];
      arr.forEach(r => { // 遍历每个字段的所有 rule
        let rule: InternalRuleItem = r;
        ...
        // 如果是函数,放到 validator 属性中
        if (typeof rule === 'function') {
          rule = {
            validator: rule,
          };
        } else {
          rule = { ...rule };
        }

        // 填充 validator 属性
        rule.validator = this.getValidationMethod(rule);
        if (!rule.validator) {
          return;
        }

        // 填充其他属性
        rule.field = z;
        rule.fullField = rule.fullField || z;
        rule.type = this.getType(rule);
        series[z] = series[z] || [];
        // 保存到 series 中
        series[z].push({
          rule,
          value,
          source,
          field: z,
        });
      });
    });
    const errorFields = {};
}

看下上边的 getValidationMethod 方法:

getValidationMethod(rule: InternalRuleItem) {
  // 如果用户自定了,直接返回自定义的
  if (typeof rule.validator === 'function') {
    return rule.validator;
  }
  const keys = Object.keys(rule);
  const messageIndex = keys.indexOf('message');
  if (messageIndex !== -1) {
    keys.splice(messageIndex, 1);
  }
  // 如果只有一个 required 字段,返回 required 的校验函数
  if (keys.length === 1 && keys[0] === 'required') {
    return validators.required;
  }
  // 否则的根据 type 去返回校验函数
  return validators[this.getType(rule)] || undefined;
}

所有的校验函数都是提前定义好的:

5597dcb9285d7037c57dde1871ed922b.png
image-20220519094003441

前端的设计模式中-策略模式 中我们也提到过上边的逻辑。

循环校验

当我们有了预处理好的所有字段的校验规则。

const series = {
  list: [
    {
      rule: {
        required: true,
        type: 'number',
        field: 'list',
        fullField: 'list',
        validator: (rule, value, callback, source, options) => {
          const errors = [];
          const validate =
            rule.required ||
            (!rule.required && source.hasOwnProperty(rule.field));
          if (validate) {
            if (value === '') {
              value = undefined;
            }
            if (isEmptyValue(value) && !rule.required) {
              return callback();
            }
            rules.required(rule, value, source, errors, options);
            if (value !== undefined) {
              rules.type(rule, value, source, errors, options);
              rules.range(rule, value, source, errors, options);
            }
          }
          callback(errors);
        },
      },
      value: '12',
      source: {
        list: '12',
        limit: 3,
      },
      field: 'list',
    },
  ],
  limit: [
    {
      rule: {
        required: true,
        message: '数量必填',
        field: 'limit',
        fullField: 'limit',
        type: 'string',
        validator: (rule, value, callback, source, options) => {
          const errors = [];
          const type = Array.isArray(value) ? 'array' : typeof value;
          rules.required(rule, value, source, errors, options, type);
          callback(errors);
        },
      },
      value: 3,
      source: {
        list: '12',
        limit: 3,
      },
      field: 'limit',
    },
    {
      rule: {
        field: 'limit',
        fullField: 'limit',
        type: 'string',
        validator(r, v, cb) {
          if (v < 100) {
            return cb(new Error('数量不能小于 100'));
          }
          cb();
        },
      },
      value: 3,
      source: {
        list: '12',
        limit: 3,
      },
      field: 'limit',
    },
  ],
};

接下来只需要搞一个双重循环,执行所有的字段和每个字段的所有校验函数。

for(const field of Object.keys(series)) { // 遍历每一个字段
  for(const data of series[field]) { // 每一个规则
    const rule = data.rule;
    const res = rule.validator(rule, data.value, cb, data.source, options);
  }
}

ruledata.valuedata.source 就是当前规则相关的变量,options 是最开始调用校验的时候传进来的 { firstFields: true },,那么 cb 是什么?

cb 函数接受一个错误数据列表,如果返回的不是数组会包装为数组,然后对错误进行填充。

最后调用 doIt 函数,将校验结果传入,后边会介绍这个方法。

function cb(e = []) {
  let errorList = Array.isArray(e) ? e : [e];
  if (errorList.length && rule.message !== undefined) {
    errorList = [].concat(rule.message); // 错误列表优先使用 message 字段
  }
  // Fill error info
  let filledErrors = errorList.map(complementError(rule, source));
  doIt(filledErrors); // 将当前字段的错误列表保存起来
}

complementError 会返回一个函数,将错误列表进行填充,主要就是补充了 fieldfieldValue 属性。

export function complementError(rule: InternalRuleItem, source: Values) {
  return (oe: ValidateError | (() => string) | string): ValidateError => {
    let fieldValue;
    if (rule.fullFields) {
      fieldValue = getValue(source, rule.fullFields);
    } else {
      fieldValue = source[(oe as any).field || rule.fullField];
    }
    if (isErrorObj(oe)) {
      oe.field = oe.field || rule.fullField;
      oe.fieldValue = fieldValue;
      return oe;
    }
    return {
      message: typeof oe === 'function' ? oe() : oe,
      fieldValue,
      field: ((oe as unknown) as ValidateError).field || rule.fullField,
    };
  };
}

收到的错误列表分为两种情况:

处理前如果 cb 收到的是 Error 列表,比如这样调用 cb(new Error('数量不能小于 100'));

那么处理前是下图:

c422ff004019d1fdf408f8ca30222e5f.png
image-20220521082112809

处理后,就会往 Error 对象中塞入 fieldfieldValue 属性。

b0c58b044064be1f390b8366d35f4004.png
image-20220521082219018

处理前如果cb 是字符串列表,比如这样调用 cb(['list is required', 'list is not a number'])

318f8a45820ae0482d08823f91008e24.png
image-20220521082345013

同样的,处理后也是塞入 fieldfieldValue 属性。

8e9db932fa9ff02fdb5658b9196cb2c0.png
image-20220521082457313

再回到我们的双重循环中。

for(const field of Object.keys(series)) { // 遍历每一个字段
  for(const data of series[field]) { // 每一个规则
    const rule = data.rule;
    const res = rule.validator(rule, data.value, cb, data.source, options);
  }
}

其中 validator 函数就是我们自己定义的:

validator(r, v, cb) {
  if (v < 100) {
    return cb(new Error('数量不能小于 100'));
  }
  cb();
},

由于 Element 官方示例是上边的样子,所以我们一般都按照上边的样子写,但其实我们也可以不调用 cb 函数,而是仅仅 return 字符串数组,或者 boolean 值,调用 cb 函数交给双重循环。

validator(r, v, cb) {
  if (v < 100) {
    return '数量不能小于 100'
  }
  return true;
},

双重循环中来处理 validator 的返回值去调用 cb 函数。

for(const field of Object.keys(series)) { // 遍历每一个字段
  for(const data of series[field]) { // 每一个规则
    const rule = data.rule;
    const res = rule.validator(rule, data.value, cb, data.source, options);
    // 根据返回的结果,去调用 cb 函数
    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);
    }
  }
}

asyncMap

向上边我们直接粗暴的写双重循环去依次校验也没有问题,但因为校验库还支持一些参数,比如前边介绍的:

2755a0d0e8ff1874b112dd6d662bce47.png
image-20220521083746778

如果是 for 循环中去处理 firstFieldsfirst 的逻辑,就过于耦合了,未来再扩充其他逻辑,双重循环中的逻辑就会越来越复杂。

async-validator  的处理方式在这里就比较优雅了,实现了 asyncMap 方法,作用就是遍历 series 数组,并且处理了 firstFieldsfirst 参数的逻辑。

下边来分析一下实现:

看一下 asyncMap 的入口参数。

export function asyncMap(
  objArr: Record<string, RuleValuePackage[]>,
  option: ValidateOption,
  func: ValidateFunc,
  callback: (errors: ValidateError[]) => void,
  source: Values,
){
  
}

接受 5 个参数:

objArr:要遍历的 rule 规则,就是我们前边生成的 series 数组,即双重循环遍历的对象。

option :最开始传入的 option,可能包含 firstFieldsfirst 属性。

func:遍历过程的中会调用这个函数,会传入当前遍历的 rule 和一个 doIt 函数,doIt 函数需要接收处理好的校验结果。这里就需要我们之前 for 循环内部的处理逻辑。

callback : 全部检验结束后调用,会传入所有的校验结果。

source:要校验的对象。

这样我们就可以把 for 循环改为直接调用 asyncMap 函数了。

asyncMap(
  series,
  options,
  (data, doIt) => {
    ...
  },
  results => {
    complete(results);
  },
  source,
);

第三个参数就是需要我们去处理 data 这个校验规则,也就是之前 for 循环中的逻辑移动过来。

其中 doIt 函数我们在之前讲的 cb 函数中调用即可。

(data, doIt) => {
  const rule = data.rule;
  rule.field = data.field;
  function cb(e: SyncErrorType | SyncErrorType[] = []) {
    let errorList = Array.isArray(e) ? e : [e];
    if (errorList.length && rule.message !== undefined) {
      errorList = [].concat(rule.message);
    }
    // Fill error info
    let filledErrors = errorList.map(complementError(rule, source));
    doIt(filledErrors); // 将当前字段的错误列表保存起来
  }
 /******** for 循环中的逻辑 *****************/
  const res = rule.validator(rule, data.value, cb, data.source, options);
  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);
  }
  /***************************************/
},

最后就是全部遍历结束后的 complete 函数,我们只需要把 results 列表传到外边即可。

function complete(results) {
  let fields: ValidateFieldsError = {};
  if (!results.length) {
    callback(null, source);
  } else {
    fields = convertFieldsError(results);
    callback (results, fields);
  }
}

上边的 callback 函数就是我们调用校验函数时候外部传入的:

const validator = new Schema(descriptor);
validator.validate(
  { list: '12', limit: null },
  { firstFields: true },
  //***** 上边的 callback ********************/
  (errors, fields) => {
    if (errors) {
      console.log('错误列表', errors);
    }
  },
  //*********************************************/
);

内层循环

双重循环的的外层是遍历所有字段,内层是遍历该字段的所有规则。

我们来先看一下内层循环的实现:

async-validator  库提供了 asyncParallelArray 方法。

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 || []));
    total++;
    if (total === arrLength) {
      callback(results);
    }
  }

  arr.forEach(a => {
    func(a, count);
  });
}

接受三个参数:

arr 就是当前字段要遍历的规则列表。

func 是处理 rule 规则的函数,内部会调用这里的 count 方法,接受当前 a 的校验结果。

传入的 func 其实就是我们前边介绍过的 for 循环内部逻辑,a 是下边的 data 参数,count 就是下边的 doIt

(data, doIt) => {
  const rule = data.rule;
  rule.field = data.field;
  function cb(e: SyncErrorType | SyncErrorType[] = []) {
    let errorList = Array.isArray(e) ? e : [e];
    if (errorList.length && rule.message !== undefined) {
      errorList = [].concat(rule.message);
    }
    // Fill error info
    let filledErrors = errorList.map(complementError(rule, source));
    doIt(filledErrors);
  }

  const res = rule.validator(rule, data.value, cb, data.source, options);
  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);
  }
},

第三个参数 callback 是当前 arr 全部校验结束后的回调,代表当前字段的所有校验规则都判断结束。

这里需要注意的是,我们是通过 count 进入的次数来判断是否去调用 callback 函数,而不是 arr 遍历结束后调用 callback

除了 asyncParallelArray 方法,因为有 firstFields 属性的存在,也就是遍历某个字段的所有规则时,如果出现校验不通过的规则就直接结束,后边的规则不再进行判断。

因此, async-validator  还提供了 asyncSerialArray 方法。

function asyncSerialArray(
  arr: RuleValuePackage[],
  func: ValidateFunc,
  callback: (errors: ValidateError[]) => void,
) {
  let index = 0;
  const arrLength = arr.length;

  function next(errors: ValidateError[]) {
    if (errors && errors.length) {
      callback(errors);
      return;
    }
    const original = index;
    index = index + 1;
    if (original < arrLength) {
      func(arr[original], next);
    } else {
      callback([]);
    }
  }

  next([]);
}

入口参数和 asyncParallelArray 是一致的,区别在于对于 arr 是顺序执行,如果过程中出现了校验不通过的规则,就直接调用 callback 结束。

外层循环

外层循环和上边很类似,其实就是遍历所有字段,然后把每个字段的校验列表传给内层循环即可。

export function asyncMap(
  objArr: Record<string, RuleValuePackage[]>,
  option: ValidateOption,
  func: ValidateFunc,
  callback: (errors: ValidateError[]) => void,
  source: Values,
) {
  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 next = (errors: ValidateError[]) => {
    results.push.apply(results, errors);
    total++;
    if (total === objArrLength) {
      callback(results);
    }
  };
  if (!objArrKeys.length) {
    callback(results);
  }
  objArrKeys.forEach(key => {
    const arr = objArr[key];
    if (firstFields.indexOf(key) !== -1) {
      asyncSerialArray(arr, func, next);
    } else {
      asyncParallelArray(arr, func, next);
    }
  });
}

入口参数前边已经介绍过了,可以看到我们做的就是遍历 objArrKeys 数组,然后根据 firstFields 的值去调用 asyncSerialArrayasyncParallelArray 。内存循环判断结束后会调用上边的 next 方法。

next 同样也是通过进入的次数,来判断是否调用 callback 函数,也就是前边介绍的 complete 方法。

和内层循环类似,因为有 first 属性的存在,也就是遍历某个字段时,存在校验不通过的字段就直接结束,后边的字段就不再进行判断。

我们只需要把所有规则打平,然后调用 asyncSerialArray 方法即可。

if (option.first) {
  const next = (errors: ValidateError[]) => {
    callback(errors);
  };
  const flattenArr = flattenObjArr(objArr);
  asyncSerialArray(flattenArr, func, next);
}

function flattenObjArr(objArr: Record<string, RuleValuePackage[]>) {
  const ret: RuleValuePackage[] = [];
  Object.keys(objArr).forEach(k => {
    ret.push(...(objArr[k] || []));
  });
  return ret;
}

代码总

以上就是  async-validator 源码的主要流程了,说起来也简单,先预处理所有规则,然后通过 asyncMap 方法双层循环遍历所有校验规则即可,这个双层循环的抽离确实很优雅,避免了循环中耦合太多逻辑。

除了上边介绍的代码,因为 async-validator 还支持 Promise 的调用风格,校验函数支持 Promise 函数等其他功能,大家感兴趣也可以到 async-validator 看一下更详细的源码。

值得一提的点是,双层循环是通过计数来判断是否结束的,而进入计数其实就是调用 cb 函数。因此如果我们规则是下边的样子:

import Schema from '../src/index';
const descriptor = {
  limit: [
    {
      validator(r, v, cb) {
        if (v < 100) {
          cb('校验1');
        }
        cb();
      },
    },
    {
      validator(r, v, cb) {
        if (v < 50) {
          return cb('校验2');
        }
        cb();
      },
    },
  ],
};
const validator = new Schema(descriptor);
validator.validate(
  { limit: 3 },
  (errors, fields) => {
    if (errors) {
      console.log('错误列表', errors);
    }
  },
);

因为我们没有传递 firstFields 属性,所以我们期望的是将 limit 所有的校验都进行了,limit 的值是 3 ,所以两个校验都没通过,应该输出下边的内容:

bb94535a53c31b93bd5316079cb81adf.png
image-20220521103342907

但其实只进行了第一个的校验:

548316e416b2bc2a985a316151e13cba.png
image-20220521103508233

原因就在于第一个 validator 进行了两次 cb ,然后内层循环的 callback 就提前调用了。

validator(r, v, cb) {
  if (v < 100) {
    cb('校验1');
  }
  cb();
},

因此我们最好保证一个 validator 只进行一次 cb ,走到 cb 后就直接 return。(因为 Element 会设置 firstFieldstrue,所以其实有多个 cb 也不影响最终结果)

validator(r, v, cb) {
  if (v < 100) {
    return cb('校验1');
  }
  cb();
},

并且一定要有一个 cb ,不然最终的回调函数永远也不会执行了,这就是为什么 Element 提示我们要进行 cb

a8a841b22904bf5b3e780702155d43a1.png
image-20220521103841307

但这里说的也不够严谨,我们也可以返回字符串,或者字符串数组、布尔值等, async-validator 内部会根据 validator 返回的结果去调用 cb 函数。

const res = rule.validator(rule, data.value, cb, data.source, options);
  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);
  }

async-validator  用计数的方式来判断是否去调用回调,就是为了实现异步的校验,当异步过程结束后才去调用 cb ,代表校验完成。

其他属性

平时写代码直接参照前人的校验规则去仿照着写了,大家也基本上是按照 Element 的样例来写校验规则,如果去  async-validator  看一下的话,会发现一些其他没听过的属性,这里也记录下。

validator 校验函数最多能接收到 5 个参数。

validator(rule, value, callback, source, options) {
  const errors = [];
  // test if email address already exists in a database
  // and add a validation error to the errors array if it does
  return errors;
},

我们可以通过第四个参数 source 拿到整个表单的对象,如果想校验一些联动的逻辑,我们就可以通过 source 拿到其他字段的值。

对对象字段的校验,如果校验字段是个对象,我们可以通过 fields 来校验对象中的字段。

const descriptor = {
  address: {
    type: 'object',
    required: true,
    fields: {
      street: { type: 'string', required: true },
      city: { type: 'string', required: true },
      zip: { type: 'string', required: true, len: 8, message: 'invalid zip' },
    },
  },
  name: { type: 'string', required: true },
};
const validator = new Schema(descriptor);
validator.validate({ address: {} }, (errors, fields) => {
  // errors for address.street, address.city, address.zip
});

transform 函数,可以将值先进行一次转换,然后再进行校验。

const descriptor = {
  name: {
    type: 'string',
    required: true,
    pattern: /^[a-z]+$/,
    transform(value) {
      return value.trim();
    },
  },
};

asyncValidator,校验函数内部是用 Promise 或者直接返回一个 Promise

const fields = {
  asyncField: {
    asyncValidator(rule, value, callback) {
      ajax({
        url: 'xx',
        value: value,
      }).then(function(data) {
        callback();
      }, function(error) {
        callback(new Error(error));
      });
    },
  },

  promiseField: {
    asyncValidator(rule, value) {
      return ajax({
        url: 'xx',
        value: value,
      });
    },
  },
};

上边就是   async-validator  开源库的核心源码了,希望对你有帮助。

e685564796777c58093383ad967b37a0.gif

················· 若川简介 ·················

你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列》20余篇,在知乎、掘金收获超百万阅读。
从2014年起,每年都会写一篇年度总结,已经坚持写了8年,点击查看年度总结
同时,最近组织了源码共读活动,帮助4000+前端人学会看源码。公众号愿景:帮助5年内前端人走向前列。

e4ae600c8d0b96ee2a90c13303753e25.png

扫码加我微信 ruochuan12、拉你进源码共读

今日话题

目前建有江西|湖南|湖北 籍 前端群,想进群的可以加我微信 ruochuan12 进群。分享、收藏、点赞、在看我的文章就是对我最大的支持~

GitHub 加速计划 / eleme / element
54.06 K
14.63 K
下载
A Vue.js 2.0 UI Toolkit for Web
最近提交(Master分支:3 个月前 )
c345bb45 7 个月前
a07f3a59 * Update transition.md * Update table.md * Update transition.md * Update table.md * Update transition.md * Update table.md * Update table.md * Update transition.md * Update popover.md 7 个月前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐