0%

async-validator源码解析(四):Schema类

上篇 —— async-validator 源码解析(三):rule —— 将 async-validator 校验库的 validator 目录下的代码进行了分析,下面继续自底向上进入到最上层index.js,分析 async-validator 校验库的核心 Schema类

由于 Schema类 的代码整体较长,所以本篇先除去代码量较大的原型链definevalidate方法的相关内容,看其他部分的结构、属性和方法。可以从仓库 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

依赖关系

代码依赖关系如下所示:

依赖关系图

按照从底向上的方式,本篇主要分析 index.js 文件中的 Schema类 及相关的 utils工具方法 、 messages.js默认消息。

Schema 类

Schema类就是 async-validator 库的标准使用方式,在文档中使用 Schema 的方式就是如下几步:

  1. 引入 async-validatorSchema
  2. 定义校验规则 descriptor
  3. new 一个 Schema 实例。
  4. 调用实例的 validate 方法(回调函数方式或者 promise 方式)。

文档中的调用 demo 如下:

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
// 官方文档中的demo

import Schema from 'async-validator';
const descriptor = {
name: {
type: 'string',
required: true,
validator: (rule, value) => value === 'muji',
},
age: {
type: 'number',
asyncValidator: (rule, value) => {
return new Promise((resolve, reject) => {
if (value < 18) {
reject('too young'); // reject 这个 error message
} else {
resolve();
}
});
},
},
};
const validator = new Schema(descriptor);
validator.validate({ name: 'muji' }, (errors, fields) => {
if (errors) {
// 校验失败,errors是一个包含所有error的数组。
// fields是一个对象,对象中field(字段)是key,每个field对应的所有error组成的数组是value。
return handleErrors(errors, fields);
}
// 校验通过
});

// PROMISE使用方法
validator
.validate({ name: 'muji', age: 16 })
.then(() => {
// 校验通过或者没有error message
})
.catch(({ errors, fields }) => {
return handleErrors(errors, fields);
});

所以首先从 Schema 的构造函数开始入手。

构造函数

构造函数分了三步:

  1. this 上定义了个实例属性 rulesnull
  2. this 上定义了一个实例私有属性 _messages,初始值为 messages.js 文件里的内容。
  3. 调用原型链 define 方法,传参 descriptor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// index.js
// ...省略

import { messages as defaultMessages, newMessages } from './messages';

/**
* 封装校验的schema
*
* @param descriptor 一个对此schema声明了校验规则的对象
*/
// Schema构造函数
function Schema(descriptor) {
// 实例的属性rules默认为空
this.rules = null;
// 实例的私有属性_messages默认为messages.js文件里的内容
this._messages = defaultMessages;
// 正式开始构建实例!
this.define(descriptor);
}

export default Schema;

原型链上的 define 方法是核心,而且代码比较长,所以本篇暂不解析 Schema.prototype.define 相关的方法,放在下一篇。

messages.js

Schema 的构造函数和静态方法中,都用到了从messages.js中引过来的 defaultMessages。可以看出,文件中都是针对不同类型的失败校验设置的提示消息模板。

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
// index.js
import { messages as defaultMessages, newMessages } from './messages';

// ...省略

// Schema的静态属性,存储了各种类型的默认message
Schema.messages = defaultMessages;

// messages.js
export function newMessages() {
return {
default: 'Validation error on field %s',
required: '%s is required',
enum: '%s must be one of %s',
whitespace: '%s cannot be empty',
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',
},
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',
date: '%s is not a %s',
boolean: '%s is not a %s',
integer: '%s is not an %s',
float: '%s is not a %s',
regexp: '%s is not a valid %s',
email: '%s is not a valid %s',
url: '%s is not a valid %s',
hex: '%s is not a valid %s',
},
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',
},
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',
},
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',
},
pattern: {
mismatch: '%s value %s does not match pattern %s',
},
clone() {
const cloned = JSON.parse(JSON.stringify(this));
cloned.clone = this.clone;
return cloned;
},
};
}

export const messages = newMessages();

message 本来就是针对不同的失败校验提供不同提示消息,所以这个 message 模板对不同的项目来说可能需要定制化。官方文档中也提供了如何给实例添加 messagedemo

1
2
3
4
5
6
7
8
9
10
// demo
import Schema from 'async-validator';
const cn = {
required: '请填写 %s',
};
const descriptor = { name: { type: 'string', required: true } };
const validator = new Schema(descriptor);
// 与defaultMessages深度合并
validator.messages(cn);
...

demo 中利用了方法Schema.prototype.message来进行 messages 的深度合并,看一下这块的代码:

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
// message方法的相关代码

// index.js
import { deepMerge } from './util';
import { messages as defaultMessages, newMessages } from './messages';

Schema.prototype = {
messages(messages) {
// 如果新的messages参数存在
if (messages) {
// 将 默认messages 和 参数 合并
this._messages = deepMerge(newMessages(), messages);
}
// 最后把合并后的messages返回
return this._messages;
},
// ......
};

// util.js

/* 深合并 */
export function deepMerge(target, source) {
// 当新messages存在时
if (source) {
// 迭代新messages
for (const s in source) {
// 确保s为自身属性key
if (source.hasOwnProperty(s)) {
const value = source[s];
// 当在原messages和新messages中这个键都为object类型时
if (typeof value === 'object' && typeof target[s] === 'object') {
// 使用扩展运算符把两个messages合并,新messages优先级高
target[s] = {
...target[s],
...value,
};
} else {
// 只要两个messages有一个不是对象,就把新messages的该属性直接赋值给老messages
target[s] = value;
}
}
}
}
return target; // 返回合并后的messages
}

可以发现此处的深度合并是有 bug 的,只能达到两级深合并,target[s]value 再往下一级就只能合并引用而不能合并值了。但是恰巧默认的 messages 只有两级的深度,所以这种写法当前情况是没有问题的。

专门看了下 git 记录,翻到了之前的issue。原来之前使用的 lodashmergeWith 进行合并,后来next的开发者提了意见,嫌弃 lodash 的包体积太大,所以作者用手写的 merge 代替了 lodashmergeWith

warning

官方文档中也给出了控制台警告信息的显示控制,就是在实例化 Schema 之前设置 warning 方法,只要把 warning 方法设置为一个空函数就能屏蔽控制台警告。

1
2
3
// demo
import Schema from 'async-validator';
Schema.warning = function () {};

下面看看源代码中关于 warning 是怎么写的。

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
// index.js
// ...省略
import { warning } from './util';

// ...省略

// Schema的静态方法,在开发环境中可以console.warn
Schema.warning = warning;

// util.js
export let warning = () => {}; // 默认warning不能输出

/* 当在非生产环境或者非node运行时,才输出warning信息 */
if (
typeof process !== 'undefined' && // Node环境
process.env &&
process.env.NODE_ENV !== 'production' && // Node环境不为生产环境
typeof window !== 'undefined' && // 确保window存在
typeof document !== 'undefined' // 确保document存在
) {
// 修改了warning函数
warning = (type, errors) => {
// 检查是否能用console
if (typeof console !== 'undefined' && console.warn) {
// 检查errors中每一个error是否都是string类型
if (errors.every((e) => typeof e === 'string')) {
console.warn(type, errors); // 打印warning
}
}
};
}

可以发现 warning 本身就被设为了一个空函数,只有在开发环境或非 node运行时,才会用 console.warn 打印出 errors 数组中的每一个 error

validators

在上一篇中已经发现,写各种各样的 rule 规则其实都是转化为 validator 校验器来处理的。所以官方给留了个静态方法用于添加自定义的 type,方便进行自定义类型的校验(但是不知道为啥官方文档中没写)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// index.js
// ...省略

import validators from './validator/index';

// ...省略

// Schema的静态属性,存储了从validator目录中引进来的众多类型的校验器
Schema.validators = validators;

// Schema的静态方法,添加一个新类型的校验器
Schema.register = function register(type, validator) {
// 校验器必须是个函数,不是就报错
if (typeof validator !== 'function') {
throw new Error(
'Cannot register a validator by type, validator is not a function'
);
}
validators[type] = validator;
};

所以在实例化之前,调用 Schema 的静态方法 register(type, validator)可以注册一个对自定义类型的校验规则。

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