0%

async-validator源码解析(二):rule

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

已完成:

  1. async-validator 源码解析(一):文档翻译
  2. async-validator 源码解析(二):rule
  3. async-validator 源码解析(三):validator
  4. async-validator 源码解析(四):Schema 类
  5. async-validator 源码解析(五):校验方法 validate

依赖关系

代码依赖关系如下所示:

依赖关系图

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

util.js

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

format

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* 格式化参数,根据第一个参数来决定怎么处理之后的参数 */
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 是数组,则空数组也算空值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 根据类型判断是否空值 */
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 是本字段对应的校验规则:

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

value 是本字段的值:如小明

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

1
2
3
4
5
6
{
name: '小明',
info: {
age: 17,
}
}

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

1
2
3
4
5
6
[
{
message: '年龄超出范围',
field: 'info.age'
}
]

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
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.jsrule 目录的统一的出口管理,这些方法的主要作用就是给 errors 数组添加对应的 error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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

校验必填字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 导入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

校验空白字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

校验是否满足合理区间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
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 < rule.min) {
// 不满足时,给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.min || val > rule.max)) {
// 参照min的分析
errors.push(
util.format(
options.messages[key].range,
rule.fullField,
rule.min,
rule.max
)
);
}
}

export default range;

pattern.js

校验正则表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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

校验枚举值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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这几种类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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 目录。

👆 全文结束,棒槌时间到 👇