async-validator源码解析(三):validator

946 阅读11分钟

上篇 async-validator 源码解析(二):ruleasync-validator 校验库的 rule 目录下的代码进行了分析,下面继续来填坑分析 validator 目录下的源码,自底向上理解表单校验的原理。可以从仓库 github.com/MageeLin/as…analysis分支看到本篇中的每个文件的代码分析。

依赖关系

代码依赖关系如下所示:

依赖关系图

按照从底向上的方式,本篇主要分析 validator 目录。

validator

validator 和之前的 rule 关系非常密切,rule 目录下方法的主要功能是通过校验 valuerule ,来给 errors 数组添加新的 error。而 validator 则是将 value 分成各种类型,然后对不同类型的 value 执行不同的 rule 校验组合,便于回调函数 callback 对最终的 errors 数组做进一步的处理。

  1. 该目录下的校验方法的结构基本类似,但是汇总的说,第一步是判断是否需要进行校验:
  • 该字段是必需的。
  • 该字段不是必需的,但是 source 对象中该字段有值且不为空值。
  1. 如果需要校验,第二步校验时是如下的步骤:
  • 先校验该字段不为空的 rule
  • 再校验该类型值对应的其他 rule
  1. 最后就执行 callback(errors),用回调函数调用 errors,使用回调函数来进行之后的步骤。

如下所示:

/**
 *  validator目录下方法的模板
 *
 *  @param rule 校验规则
 *  @param value 该字段在source对象中的值
 *  @param callback 回调函数
 *  @param source 要校验的source对象
 *  @param options 校验选项
 *  @param options.messages 校验message
 */
// foo是各种各样不同类型
function foo(rule, value, callback, source, options) {
  const errors = []; // 初始化errors数组
  // 判断是否需要校验,这里分了两种情况
  // 第一种是该字段是必需的
  // 第二种是该字段不必需,但是source对象中该字段有值,也就是该字段被填写了。
  const validate =
    rule.required || (!rule.required && source.hasOwnProperty(rule.field));
  // 如果需要校验时
  if (validate) {
    // 下面的if是对第二种情况又细分
    // 如果对该类型不必需验证且该字段有值,但是值为空值,就需要单独拎出来
    if (isEmptyValue(value, 'foo') && !rule.required) {
      return callback(); // 就直接callback一个undefined
    }
    // 下面就是普通情况的处理了
    rules.required(rule, value, source, errors, options); // 正式校验之前先把字段必需的required规则校验
    // 当值不为undefined时,再进行对应的各种不同类型的规则校验
    if (value !== undefined) {
      rules.foo(rule, value, source, errors, options);
    }
  }
  // 最后用回调函数调用errors,这也是validator目录下文件的最终目的
  callback(errors);
}

其实回想一下,在 Element-UI 中,我们为字段创建自定义的 validator 函数,参数也是 validator(rule, value, callback) {...}(专门测试了下第 4 和第 5 个参数的确也是 sourceoptions),与 validator 目录中的各类型校验方法是一样的入参。而且校验成功时同样是什么都不返回或者 callback(),校验失败时同样是返回 callback(errors)。所以在手写 validator 时就是在手写这个这个模板,rule 中的那些自定义参数也就是 validator 的类似语法糖简写。

string.js

下面以 string.js 为例,说明上述模板是如何用的:

import rules from '../rule/index.js';
import { isEmptyValue } from '../util';

/**
 *  执行字符串类型校验
 *
 *  @param rule 校验规则
 *  @param value 该字段在source对象中的值
 *  @param callback 回调函数
 *  @param source 要校验的source对象
 *  @param options 校验选项
 *  @param options.messages 校验message
 */
function string(rule, value, callback, source, options) {
  const errors = [];
  const validate =
    rule.required || (!rule.required && source.hasOwnProperty(rule.field));
  if (validate) {
    // 注意这里是专门检验的string空值
    if (isEmptyValue(value, 'string') && !rule.required) {
      return callback();
    }
    rules.required(rule, value, source, errors, options, 'string');
    if (!isEmptyValue(value, 'string')) {
      // 先校验类型规则 rule.type
      rules.type(rule, value, source, errors, options);
      // 再校验范围规则 rule.len max min
      rules.range(rule, value, source, errors, options);
      // 再校验模式规则 rule.pattern
      rules.pattern(rule, value, source, errors, options);
      if (rule.whitespace === true) {
        // 当rule.whitespace为true时,还要校验whitespace规则
        rules.whitespace(rule, value, source, errors, options);
      }
    }
  }
  // 调用回调函数
  callback(errors);
}

export default string;

index.js

index.jsvalidator 目录的统一出口管理,有一个比较有意思的地方是 urlhexemail 这三种类型的校验其实本质上都是 type校验。

import string from './string';
import method from './method';
import number from './number';
import boolean from './boolean';
import regexp from './regexp';
import integer from './integer';
import float from './float';
import array from './array';
import object from './object';
import enumValidator from './enum';
import pattern from './pattern';
import date from './date';
import required from './required';
import type from './type';
import any from './any';

/**
 *  校验方法的统一出口管理
 *
 *  @param rule 校验规则
 *  @param value 该字段在source对象中的值
 *  @param callback 回调函数
 *  @param source 要校验的source对象
 *  @param options 校验选项
 *  @param options.messages 校验message
 */

export default {
  string,
  method,
  number,
  boolean,
  regexp,
  integer,
  float,
  array,
  object,
  enum: enumValidator,
  pattern,
  date,
  url: type, // 可以发现url hex 和 email三种类型都交给typeValidator来处理了
  hex: type,
  email: type,
  required,
  any,
};

any.js

校验任意类型只需要一步,校验不为空即可。

import rules from '../rule/index.js';
import { isEmptyValue } from '../util';

/**
 *  校验任意类型
 *
 *  @param rule 校验规则
 *  @param value 该字段在source对象中的值
 *  @param callback 回调函数
 *  @param source 要校验的source对象
 *  @param options 校验选项
 *  @param options.messages 校验message
 */
function any(rule, value, callback, source, options) {
  const errors = [];
  const validate =
    rule.required || (!rule.required && source.hasOwnProperty(rule.field));
  if (validate) {
    if (isEmptyValue(value) && !rule.required) {
      return callback();
    }
    // 任意类型,所以只用校验该值存在即可
    rules.required(rule, value, source, errors, options);
  }
  callback(errors);
}

export default any;

array.js

校验数组需要三步。第一步校验非空数组,第二步校验类型,第三步校验范围。

import rules from '../rule/index';
import { isEmptyValue } from '../util';
/**
 *  校验数组
 *
 *  @param rule 校验规则
 *  @param value 该字段在source对象中的值
 *  @param callback 回调函数
 *  @param source 要校验的source对象
 *  @param options 校验选项
 *  @param options.messages 校验message
 */
function array(rule, value, callback, source, options) {
  const errors = [];
  const validate =
    rule.required || (!rule.required && source.hasOwnProperty(rule.field));
  if (validate) {
    if (isEmptyValue(value, 'array') && !rule.required) {
      return callback();
    }
    rules.required(rule, value, source, errors, options, 'array');
    // 当值为非空数组时
    if (!isEmptyValue(value, 'array')) {
      // 先校验类型 rule.type
      rules.type(rule, value, source, errors, options);
      // 再校验范围 rule.len max min
      rules.range(rule, value, source, errors, options);
    }
  }
  callback(errors);
}

export default array;

boolean.js

校验布尔值需要两步。第一步校验不为空值,第二步校验类型。

import { isEmptyValue } from '../util';
import rules from '../rule/index.js';

/**
 *  校验布尔值
 *
 *  @param rule 校验规则
 *  @param value 该字段在source对象中的值
 *  @param callback 回调函数
 *  @param source 要校验的source对象
 *  @param options 校验选项
 *  @param options.messages 校验message
 */
function boolean(rule, value, callback, source, options) {
  const errors = [];
  const validate =
    rule.required || (!rule.required && source.hasOwnProperty(rule.field));
  if (validate) {
    if (isEmptyValue(value) && !rule.required) {
      return callback();
    }
    rules.required(rule, value, source, errors, options);
    if (value !== undefined) {
      // 校验类型
      rules.type(rule, value, source, errors, options);
    }
  }
  callback(errors);
}

export default boolean;

date.js

校验时间需要三步。第一步校验不为空,第二步校验类型,第三步校验范围。

import rules from '../rule/index.js';
import { isEmptyValue } from '../util';

/**
 * 校验时间
 *
 *  @param rule 校验规则
 *  @param value 该字段在source对象中的值
 *  @param callback 回调函数
 *  @param source 要校验的source对象
 *  @param options 校验选项
 *  @param options.messages 校验message
 */
function date(rule, value, callback, source, options) {
  // console.log('integer rule called %j', rule);
  const errors = [];
  const validate =
    rule.required || (!rule.required && source.hasOwnProperty(rule.field));
  // console.log('validate on %s value', value);
  if (validate) {
    if (isEmptyValue(value, 'date') && !rule.required) {
      return callback();
    }
    rules.required(rule, value, source, errors, options);
    // 当是非空 时间值时
    if (!isEmptyValue(value, 'date')) {
      let dateObject;

      // 不管是时间字符串还是时间实例,都格式化成Date的实例
      if (value instanceof Date) {
        dateObject = value;
      } else {
        dateObject = new Date(value);
      }

      rules.type(rule, dateObject, source, errors, options);
      if (dateObject) {
        // 校验时间戳是否在范围内 rule.len max min
        rules.range(rule, dateObject.getTime(), source, errors, options);
      }
    }
  }
  callback(errors);
}

export default date;

enum.js

校验枚举值需要两步,第一步校验不为空,第二步校验枚举。

import rules from '../rule/index.js';
import { isEmptyValue } from '../util';

const ENUM = 'enum';

/**
 *  校验枚举值列表
 *
 *  @param rule 校验规则
 *  @param value 该字段在source对象中的值
 *  @param callback 回调函数
 *  @param source 要校验的source对象
 *  @param options 校验选项
 *  @param options.messages 校验message
 */
function enumerable(rule, value, callback, source, options) {
  const errors = [];
  const validate =
    rule.required || (!rule.required && source.hasOwnProperty(rule.field));
  if (validate) {
    if (isEmptyValue(value) && !rule.required) {
      return callback();
    }
    rules.required(rule, value, source, errors, options);
    if (value !== undefined) {
      // 校验枚举值规则
      rules[ENUM](rule, value, source, errors, options);
    }
  }
  callback(errors);
}

export default enumerable;

float.js

校验浮点数需要三步。第一步校验不为空,第二步校验类型,第三步校验范围。

import rules from '../rule/index.js';
import { isEmptyValue } from '../util';

/**
 *  校验浮点数
 *
 *  @param rule 校验规则
 *  @param value 该字段在source对象中的值
 *  @param callback 回调函数
 *  @param source 要校验的source对象
 *  @param options 校验选项
 *  @param options.messages 校验message
 */
function floatFn(rule, value, callback, source, options) {
  const errors = [];
  const validate =
    rule.required || (!rule.required && source.hasOwnProperty(rule.field));
  if (validate) {
    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);
}

export default floatFn;

integer.js

校验整数需要三步。第一步校验不为空,第二步校验类型,第三步校验范围。

import rules from '../rule/index.js';
import { isEmptyValue } from '../util';

/**
 * 校验一个数字是否为整数
 *
 *  @param rule 校验规则
 *  @param value 该字段在source对象中的值
 *  @param callback 回调函数
 *  @param source 要校验的source对象
 *  @param options 校验选项
 *  @param options.messages 校验message
 */
function integer(rule, value, callback, source, options) {
  const errors = [];
  const validate =
    rule.required || (!rule.required && source.hasOwnProperty(rule.field));
  if (validate) {
    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);
}

export default integer;

method.js

校验浮点数需要两步。第一步校验不为空,第二步校验类型。

import rules from '../rule/index.js';
import { isEmptyValue } from '../util';

/**
 *  校验函数
 *
 *  @param rule 校验规则
 *  @param value 该字段在source对象中的值
 *  @param callback 回调函数
 *  @param source 要校验的source对象
 *  @param options 校验选项
 *  @param options.messages 校验message
 */
function method(rule, value, callback, source, options) {
  const errors = [];
  const validate =
    rule.required || (!rule.required && source.hasOwnProperty(rule.field));
  if (validate) {
    if (isEmptyValue(value) && !rule.required) {
      return callback();
    }
    rules.required(rule, value, source, errors, options);
    if (value !== undefined) {
      // 只校验类型规则
      rules.type(rule, value, source, errors, options);
    }
  }
  callback(errors);
}

export default method;

object.js

校验对象需要两步。第一步校验不为空,第二步校验类型。

import rules from '../rule/index.js';
import { isEmptyValue } from '../util';

/**
 *  校验对象
 *
 *  @param rule 校验规则
 *  @param value 该字段在source对象中的值
 *  @param callback 回调函数
 *  @param source 要校验的source对象
 *  @param options 校验选项
 *  @param options.messages 校验message
 */
function object(rule, value, callback, source, options) {
  const errors = [];
  const validate =
    rule.required || (!rule.required && source.hasOwnProperty(rule.field));
  if (validate) {
    if (isEmptyValue(value) && !rule.required) {
      return callback();
    }
    rules.required(rule, value, source, errors, options);
    if (value !== undefined) {
      // 校验类型规则
      rules.type(rule, value, source, errors, options);
    }
  }
  callback(errors);
}

export default object;

pattern.js

校验 pattern 需要两步。第一步校验不为空,第二步校验 pattern。

import rules from '../rule/index.js';
import { isEmptyValue } from '../util';

/**
 *  校验pattern
 *
 *
 * 当rule中只包含pattern而不规定type为string时,执行这个校验
 *
 *  @param rule 校验规则
 *  @param value 该字段在source对象中的值
 *  @param callback 回调函数
 *  @param source 要校验的source对象
 *  @param options 校验选项
 *  @param options.messages 校验message
 */
function pattern(rule, value, callback, source, options) {
  const errors = [];
  const validate =
    rule.required || (!rule.required && source.hasOwnProperty(rule.field));
  if (validate) {
    if (isEmptyValue(value, 'string') && !rule.required) {
      return callback();
    }
    rules.required(rule, value, source, errors, options);
    if (!isEmptyValue(value, 'string')) {
      // 校验正则表达式规则
      rules.pattern(rule, value, source, errors, options);
    }
  }
  callback(errors);
}

export default pattern;

regexp.js

校验正则表达式需要两步。第一步校验不为空,第二步校验类型。

import rules from '../rule/index.js';
import { isEmptyValue } from '../util';

/**
 *  校验正则表达式本身
 *
 *  @param rule 校验规则
 *  @param value 该字段在source对象中的值
 *  @param callback 回调函数
 *  @param source 要校验的source对象
 *  @param options 校验选项
 *  @param options.messages 校验message
 */
function regexp(rule, value, callback, source, options) {
  const errors = [];
  const validate =
    rule.required || (!rule.required && source.hasOwnProperty(rule.field));
  if (validate) {
    if (isEmptyValue(value) && !rule.required) {
      return callback();
    }
    rules.required(rule, value, source, errors, options);
    if (!isEmptyValue(value)) {
      // 校验类型规则
      rules.type(rule, value, source, errors, options);
    }
  }
  callback(errors);
}

export default regexp;

required.js

校验整数只需要一步,校验不为空。

import rules from '../rule/index.js';

/**
 * 校验必需字段
 *
 *  @param rule 校验规则
 *  @param value 该字段在source对象中的值
 *  @param callback 回调函数
 *  @param source 要校验的source对象
 *  @param options 校验选项
 *  @param options.messages 校验message
 */
function required(rule, value, callback, source, options) {
  // 初始化errors数组
  const errors = [];
  // 如果是数组,type就给‘array’,不是数组就typeof来求
  // typeof的结果:undefined object boolean number bigint string symbol function
  const type = Array.isArray(value) ? 'array' : typeof value;
  // 这里去调用rules.required来给errors数组添加error
  rules.required(rule, value, source, errors, options, type);
  // 用回调函数来处理errors数组
  callback(errors);
}

export default required;

number.js

校验数字需要三步。第一步校验不为空,第二步校验类型,第三步校验范围。

import rules from '../rule/index.js';
import { isEmptyValue } from '../util';

/**
 *  校验数字
 *
 *  @param rule 校验规则
 *  @param value 该字段在source对象中的值
 *  @param callback 回调函数
 *  @param source 要校验的source对象
 *  @param options 校验选项
 *  @param options.messages 校验message
 */
function number(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) {
      // 先校验类型规则 rule.type
      rules.type(rule, value, source, errors, options);
      // 再校验范围规则 rule.len min max
      rules.range(rule, value, source, errors, options);
    }
  }
  callback(errors);
}

export default number;

type.js

校验整数需要两步。第一步校验不为空,第二步校验类型。

import rules from '../rule/index.js';
import { isEmptyValue } from '../util';

/**
 * 校验类型
 *
 *  @param rule 校验规则
 *  @param value 该字段在source对象中的值
 *  @param callback 回调函数
 *  @param source 要校验的source对象
 *  @param options 校验选项
 *  @param options.messages 校验message
 */
function type(rule, value, callback, source, options) {
  const ruleType = rule.type;
  const errors = [];
  const validate =
    rule.required || (!rule.required && source.hasOwnProperty(rule.field));
  if (validate) {
    if (isEmptyValue(value, ruleType) && !rule.required) {
      return callback();
    }
    rules.required(rule, value, source, errors, options, ruleType);
    if (!isEmptyValue(value, ruleType)) {
      // 校验type规则 rule.type
      rules.type(rule, value, source, errors, options);
    }
  }
  callback(errors);
}

export default type;

总结一下:

  • 校验一步 —— 不为空requiredany
  • 校验两步 —— 不为空 -> 类型typeregexppatternobjectmethodbooleanenum 的第二步是校验枚举。
  • 校验三步 —— 不为空 -> 类型 -> 范围numberintegerfloatdatearray