elementui源码_asyncvalidator源码解析(二):rule

上篇 async-validator 源码解析(一):文档翻译 已经将ElementUIAnt Design都依赖的async-validator校验库的文档进行了翻译,下面继续来填坑分析 async-validator 的源码,理解表单校验的原理。可以从仓库 https://github.com/MageeLin/async-validator-source-code-analysis 的analysis分支看到本篇中的每个文件的代码分析。

依赖关系

代码依赖关系如下所示:

eff33acf3bf1ec878b7979da7409b20c.png

依赖关系图

按照从下到上的方式,本篇主要分析rule目录和依赖的util.js中的部分工具函数。

util.js

从文件名和依赖关系就能清晰的发现,util.js 是一个典型的工具函数库。rule 目录中主要依赖到的是 format 和 isEmptyValue 两个方法。

format

format 函数的作用是格式化参数,可以接收无数个参数,返回一个字符串,但是它是利用第一个入参来判断如何格式化。第一个参数是 function 类型,就直接执行这个格式化函数来返回字符串 message;第一个参数若是 string 类型,就根据占位符和参数返回格式化后的字符串 message

/* 格式化参数,根据第一个参数来决定怎么处理之后的参数 */
export function format(...args) {
  let i = 1;
  const f = args[0];
  const len = args.length;
  // 当第一个参数是function时
  if (typeof f === 'function') {
    return f.apply(null, args.slice(1)); // 把剩余参数给f调用
  }
  // 当第一个参数是string时
  if (typeof f === 'string') {
    // 根据字符串标志来区分处理方式
    let str = String(f).replace(formatRegExp, (x) => {
      if (x === '%%') {
        return '%'; // 如果是%%,就返回%
      }
      if (i >= len) {
        return x;
      }
      switch (x) {
        case '%s':
          return String(args[i++]); // 如果是%s,就返回字符串化的结果
        case '%d':
          return Number(args[i++]); // 如果是%d,就返回数字化的结果
        case '%j':
          try {
            return JSON.stringify(args[i++]); // 如果是%j,就返回JSON
          } catch (_) {
            return '[Circular]';
          }
          break;
        default:
          return x; // 默认原样返回
      }
    });
    return str; // 返回处理后的结果
  }
  return f;
}

isEmptyValue

这一个方法是来判断是否为空值,但是它把字符串和数组单独拿了出来。如果 value 是字符串,则空字符串也算空值;如果 value 是数组,则空数组也算空值。

/* 根据类型判断是否空值 */
export function isEmptyValue(value, type) {
  // value为undefined或null时肯定是空值
  if (value === undefined || value === null) {
    return true;
  }
  // 数组类型,长度为0,肯定空值
  if (type === 'array' && Array.isArray(value) && !value.length) {
    return true;
  }
  // 原始的字符串类型,空字符串就为空值
  if (isNativeStringType(type) && typeof value === 'string' && !value) {
    return true;
  }
  return false; // 其他情况都认为不空
}

rule

rule 目录中的每一个文件都 export 一个方法,这些方法的入参大致相同,如下:

  • @param rule 校验的规则
  • @param value source 对象中该字段的值
  • @param source 要校验的 source 对象
  • @param errors 本次校验将要去添加的 errors 数组
  • @param options 校验选项
  • @param options.messages 校验的 messages

这些方法的作用都是一样的,就是校验。如果校验通过,就继续执行;如果校验不通过,就给 errors 数组添加一个对应的新 error

这些入参的格式基本一致,举几个例子:

rule 是本字段对应的校验规则:

{
  field: "name",
  fullField: "name",
  message: "姓名为必填项",
  required: false,
  type: "string",
  validator: ƒ required$1(rule, value, callback, source, options)
}

value 是本字段的值:如小明

source 是要校验的整个 source 对象:

{
  name: '小明',
  info: {
    age: 17,
  }
}

errors 是本次校验将要去添加的 errors 数组,假设之前没有 error,则 errors 为[],如果之前已经存在了一些 error,则格式如下所示:

[
  {
    message: '年龄超出范围',
    field: 'info.age',
  }
]

options 是该字段校验时的选项,当 message 属性为默认值时,格式如下:

{
  firstFields: true,
  messages: {
    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"},
    clone: ƒ clone(),
    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"},
    default: "Validation error on field %s",
    enum: "%s must be one of %s",
    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"},
    pattern: {mismatch: "%s value %s does not match pattern %s"},
    required: "%s is required",
    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"},
    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", …},
    whitespace: "%s cannot be empty",
  }
}

index.js

index.js 是 rule 目录的统一的出口管理,这些方法的主要作用就是给 errors 数组添加对应的 error

import required from './required';
import whitespace from './whitespace';
import type from './type';
import range from './range';
import enumRule from './enum';
import pattern from './pattern';

/**
 * 统一的出口管理,这些方法的主要作用就是给errors数组添加对应的error
 *
 *  @param rule 校验的规则
 *  @param value source对象中该字段的值
 *  @param source 要校验的source对象
 *  @param errors 本次校验将要去添加的errors数组
 *  @param options 校验选项
 *  @param options.messages 校验的messages
 */
export default {
  required,
  whitespace,
  type,
  range,
  enum: enumRule,
  pattern,
};

required.js

校验必填字段。

// 导入util
import * as util from '../util';

/**
 * 校验必填字段的规则
 *
 *  @param rule 校验的规则
 *  @param value source对象中该字段的值
 *  @param source 要校验的source对象
 *  @param errors 本次校验将要去添加的errors数组
 *  @param options 校验选项
 *  @param options.messages 校验的messages
 */
function required(rule, value, source, errors, options, type) {
  // rule的required字段为true 且
  // (source对象中没有这个字段 或 根据类型判断这个字段为空值)
  if (
    rule.required &&
    (!source.hasOwnProperty(rule.field) ||
      util.isEmptyValue(value, type || rule.type))
  ) {
    // 此时就在errors数组中添加一个格式化后的error
    // options.messages.required,默认的是 %s is required
    // rule.fullField是完全的字段路径,比如 a.b,
    // 此时经过format格式化后就变成 a.b is required
    errors.push(util.format(options.messages.required, rule.fullField));
  }
}

export default required;

whitespace.js

校验空白字符。

import * as util from '../util';

/**
 *  校验空白字符的规则
 *
 *  @param rule 校验的规则
 *  @param value source对象中该字段的值
 *  @param source 要校验的source对象
 *  @param errors 本次校验将要去添加的errors数组
 *  @param options 校验选项
 *  @param options.messages 校验的messages
 */
function whitespace(rule, value, source, errors, options) {
  // 用正则表达式^\s+$来测试该值为真 或 该值直接为空
  if (/^\s+$/.test(value) || value === '') {
    // options.messages.whitespace 默认为 %s cannot be empty
    // fullField依然是完全的路径
    errors.push(util.format(options.messages.whitespace, rule.fullField));
  }
}

export default whitespace;

range.js

校验是否满足合理区间。

import * as util from '../util';

/**
 *  校验是否满足最大最小值合理区间的的规则
 *
 *  @param rule 校验的规则
 *  @param value source对象中该字段的值
 *  @param source 要校验的source对象
 *  @param errors 本次校验将要去添加的errors数组
 *  @param options 校验选项
 *  @param options.messages 校验的messages
 */
function range(rule, value, source, errors, options) {
  // rule中的len、min、max是否存在并且是数字类型
  const len = typeof rule.len === 'number';
  const min = typeof rule.min === 'number';
  const max = typeof rule.max === 'number';
  // 正则匹配码点范围从U+010000一直到U+10FFFF的文字(补充平面Supplementary Plane)
  const spRegexp = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
  let val = value;
  let key = null;
  // value是否是number类型 string类型或者数组类型
  const num = typeof value === 'number';
  const str = typeof value === 'string';
  const arr = Array.isArray(value);
  // 把类型名赋给key变量
  if (num) {
    key = 'number';
  } else if (str) {
    key = 'string';
  } else if (arr) {
    key = 'array';
  }
  // 如果值不是支持范围校验的类型
  // 那么校验规则应该type属性来测试特定的类型
  if (!key) {
    // 不是这三种类型,就直接返回false
    return false;
  }
  // 如果是value是数组类型,val设为数组长度
  if (arr) {
    val = value.length;
  }
  // 如果value是string类型,val设为字符串长度
  if (str) {
    // 处理码点大于U+010000的文字length属性不准确的bug,如"???".lenght !== 3
    val = value.replace(spRegexp, '_').length;
  }
  // 到这一步时,如果是数字类型,自然val就是那个数字

  // 如果规则中len属性存在,就优先len属性来匹配
  if (len) {
    if (val !== rule.len) {
      // 参照min的分析
      errors.push(
        util.format(options.messages[key].len, rule.fullField, rule.len)
      );
    }
    // 不存在len属性时的比较都是开区间
    // 只有min属性存在,就看看是否满足大于min的条件
  } else if (min && !max && val     // 不满足时,给errors数组添加一个error
    errors.push(
      // 对于三种类型不同的情况,options.messages[key].min给出的默认值不一致
      // options.messages['array'].min是"%s cannot be less than %s in length"
      // options.messages['number'].min是"%s cannot be less than %s"
      // options.messages['string'].min是"%s must be at least %s characters"

      // 第二个参数还是完全路径的name,比如a.b

      // 第三个参数是min值,比如10

      // 这样format格式化后返回的结果就是“a.b cannot be less than 10”
      util.format(options.messages[key].min, rule.fullField, rule.min)
    );
    // 只有max属性存在,就看看是否满足小于max的条件
  } else if (max && !min && val > rule.max) {
    // 参照min的分析
    errors.push(
      util.format(options.messages[key].max, rule.fullField, rule.max)
    );
    // min和max属性都存在,就看看是否满足大于min且小于max的条件
  } else if (min && max && (val  rule.max)) {
    // 参照min的分析
    errors.push(
      util.format(
        options.messages[key].range,
        rule.fullField,
        rule.min,
        rule.max
      )
    );
  }
}

export default range;

pattern.js

校验正则表达式。

import * as util from '../util';

/**
 *  校验正则表达式的规则
 *
 *  @param rule 校验的规则
 *  @param value source对象中该字段的值
 *  @param source 要校验的source对象
 *  @param errors 本次校验将要去添加的errors数组
 *  @param options 校验选项
 *  @param options.messages 校验的messages
 */
function pattern(rule, value, source, errors, options) {
  // 当rule中pattern属性存在时
  if (rule.pattern) {
    // 如果pattern是正则表达式的话
    if (rule.pattern instanceof RegExp) {
      // 如果 pattern 是一个 RegExp 实例,则重置'lastIndex'以防它的'global'标志意外地被设置为'true',
      // 这在校验场景中不是必需的,结果可能会产生误导
      rule.pattern.lastIndex = 0;
      // 如果正则测试不通过
      if (!rule.pattern.test(value)) {
        // options.messages.pattern.mismatch 默认值为 %s value %s does not match pattern %s
        // 三个%s的参数为后三个
        errors.push(
          util.format(
            options.messages.pattern.mismatch,
            rule.fullField,
            value,
            rule.pattern
          )
        );
      }
      // 如果pattern是string类型的话
    } else if (typeof rule.pattern === 'string') {
      // 就直接用字符串来生成正则表达式_pattern
      // 其余部分与上面同理
      const _pattern = new RegExp(rule.pattern);
      if (!_pattern.test(value)) {
        errors.push(
          util.format(
            options.messages.pattern.mismatch,
            rule.fullField,
            value,
            rule.pattern
          )
        );
      }
    }
  }
}

export default pattern;

enum.js

校验枚举值。

import * as util from '../util';

const ENUM = 'enum';

/**
 *  校验值是否存在在枚举值列表中的规则
 *
 *  @param rule 校验的规则
 *  @param value source对象中该字段的值
 *  @param source 要校验的source对象
 *  @param errors 本次校验将要去添加的errors数组
 *  @param options 校验选项
 *  @param options.messages 校验的messages
 */
function enumerable(rule, value, source, errors, options) {
  // 先检查rule中的enum属性是否为一个数组,不是的话就改为空数组
  rule[ENUM] = Array.isArray(rule[ENUM]) ? rule[ENUM] : [];
  // 在数组中搜索value
  if (rule[ENUM].indexOf(value) === -1) {
    // 搜不到value时就给errors数组添加一个error
    errors.push(
      // options.messages[ENUM]的默认值是%s must be one of %s
      // 后两个参数就是两个%s占位所代表的参数
      util.format(options.messages[ENUM], rule.fullField, rule[ENUM].join(', '))
    );
  }
}

export default enumerable;

type.js

校验值类型。这里比较有意思,用了一些比较简单的判断组合,将值分为了integerfloatarrayregexpobjectmethodemailnumberdateurlhex这几种类型。

import * as util from '../util';
import required from './required';

/* eslint max-len:0 */

// 定义几种正则表达式 email、url和hex
const pattern = {
  // http://emailregex.com/
  email: /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
  url: new RegExp(
    '^(?!mailto:)(?:(?:http|https|ftp)://|//)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$',
    'i'
  ),
  hex: /^#?([a-f0-9]{6}|[a-f0-9]{3})$/i,
};

// 定义一些类型检测工具函数
const types = {
  // 整数
  integer(value) {
    return types.number(value) && parseInt(value, 10) === value;
  },
  // 浮点数
  float(value) {
    return types.number(value) && !types.integer(value);
  },
  // 数组
  array(value) {
    return Array.isArray(value);
  },
  // 正则表达式
  regexp(value) {
    if (value instanceof RegExp) {
      return true;
    }
    try {
      return !!new RegExp(value);
    } catch (e) {
      return false;
    }
  },
  // 时间
  date(value) {
    return (
      typeof value.getTime === 'function' &&
      typeof value.getMonth === 'function' &&
      typeof value.getYear === 'function' &&
      !isNaN(value.getTime())
    );
  },
  // 数字
  number(value) {
    if (isNaN(value)) {
      return false;
    }
    return typeof value === 'number';
  },
  // 对象,并且不能是数组
  object(value) {
    return typeof value === 'object' && !types.array(value);
  },
  // 方法
  method(value) {
    return typeof value === 'function';
  },
  // email
  email(value) {
    return (
      typeof value === 'string' &&
      !!value.match(pattern.email) &&
      value.length 255
    );
  },
  // url
  url(value) {
    return typeof value === 'string' && !!value.match(pattern.url);
  },
  // 十六进制
  hex(value) {
    return typeof value === 'string' && !!value.match(pattern.hex);
  },
};

/**
 * 校验值类型的规则
 *
 *  @param rule 校验的规则
 *  @param value source对象中该字段的值
 *  @param source 要校验的source对象
 *  @param errors 本次校验将要去添加的errors数组
 *  @param options 校验选项
 *  @param options.messages 校验的messages
 */
function type(rule, value, source, errors, options) {
  // 当rule中required属性为真并且value存在时
  if (rule.required && value === undefined) {
    // 先调用required来验证对于要求的类型是否不为空值
    required(rule, value, source, errors, options);
    return;
  }
  const custom = [
    'integer',
    'float',
    'array',
    'regexp',
    'object',
    'method',
    'email',
    'number',
    'date',
    'url',
    'hex',
  ];
  const ruleType = rule.type;
  // 如果custom数组中存在rule.type这种类型
  if (custom.indexOf(ruleType) > -1) {
    // 调用对应的类型检查工具函数
    if (!types[ruleType](value)) {
      // 检查失败的话就添加新的error
      errors.push(
        // options.messages.types[ruleType]的默认值有很多种,比如%s is not an %s
        util.format(options.messages.types[ruleType], rule.fullField, rule.type)
      );
    }
    // 如果custom数组中不存在rule.type这种类型,就进行直接的原生类型检查 ??存疑??
  } else if (ruleType && typeof value !== rule.type) {
    errors.push(
      util.format(options.messages.types[ruleType], rule.fullField, rule.type)
    );
  }
}

export default type;

rule 目录分析完成,下一篇继续向上填坑 validator 目录。

a3c04e086bc78a871796f11c67ef7921.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值