上篇 async-validator 源码解析(一):文档翻译 已经将ElementUI
和Ant Design
都依赖的async-validator
校验库的文档进行了翻译,下面继续来填坑分析 async-validator 的源码,理解表单校验的原理。可以从仓库 https://github.com/MageeLin/async-validator-source-code-analysis 的analysis
分支看到本篇中的每个文件的代码分析。
依赖关系
代码依赖关系如下所示:
按照从下到上的方式,本篇主要分析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
校验值类型。这里比较有意思,用了一些比较简单的判断组合,将值分为了integer
、float
、array
、regexp
、object
、method
、email
、number
、date
、url
、hex
这几种类型。
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
目录。