表单是前端开发中经常使用的组件,在表单使用的过程中不可避免地会遇到表单校验的场景。
在element-ui中提供了较为简洁易用的表单校验方法,很多同学会模仿样例使用,但是并不了解其内部实现的原理,因此在很多情况下出现问题后不知道如何定位解决。
这篇文章旨在通过源码解析介绍elment-ui中的表单校验是如何实现的,从而提升大家对表单校验的理解。
一、async-validator
在介绍表单校验之前,我们需要先了解async-validator。
async-validator是一个异步校验的功能库,可以根据传入的规则和数据输出校验结果。
目前大多数组件库(包括element-ui)内部都是引入async-validator完成数据校验的,即表单中的每一项都是用async-validator来进行校验的。
看一下async-validator的基本使用方法
import asyncValidator from 'async-validator'
var descriptor = {
name: {
type: "string",
required: true,
validator: (rule, value, callback) => {
if (value === "Ezrel") {
//
callback();
} else {
callback(new Error("Wrong name"));
}
}
}
};
var validator = new asyncValidator(descriptor);
// callback USAGE
validator.validate(
{ name: "Jinx" },
{ firstFields: true },// 串行校验,遇到错误就停止校验
(errors, fields) => {
if (errors) {
// validation failed, errors is an array of all errors
// fields is an object keyed by field name with an array of
// errors per field
console.log("validate error =>", errors);
} else {
// validation passed
console.log("validate success!");
}
}
);
}
这里如果不是很清楚也没关系,只需要知道async-validator的用处就是传入值和规则,会返回校验的结果即可,不影响后续的阅读。
(值得注意的是,async-validator在web和node端的使用略有差异,这里我们关注的是web端的使用)
二、表单项Form-item
整个表单的校验其实是其所包含的每个表单项校验的结果集合,因此先看一下每个表单项是如何校验的。
2.1 表单项校验规则
getRules方法用于获取当前表单项的校验规则,我们看一下代码
// element-ui\packages\form\src\form-item.vue
getRules() {
let formRules = this.form.rules; // 整个表单的rules属性
const selfRules = this.rules; // 当前表单项的rules属性
const requiredRule = this.required !== undefined ? { required: !!this.required } : []; // 当前表单项的required属性
const prop = getPropByPath(formRules, this.prop || '');
formRules = formRules ? (prop.o[this.prop || ''] || prop.v) : [];// 根据当期表单项的prop属性,从整个表单的rules中获取对应的规则
return [].concat(selfRules || formRules || []).concat(requiredRule);// 校验规则组合
},
从以上代码中可以分析出几点:
1、表单项自身的rules规则优先级高于整个表单上的rules规则(selfRules || formRules || []);
2、如果表单项自身没有rules规则,则从表单的rules规则中取prop属性对应的规则(说明prop属性在表单校验中是非常重要,如果没有给表单项传递prop属性,则无法从表单的规则中拿到对应表单项的规则;实际上表单项校验时的值也是通过prop属性从表单的model属性中读取的);
3、表单项上的required属性与rules中的required具有同等效果。
2.2 trigger属性
在rules中我们可以设置trigger属性,trigger表示校验的触发条件,可以设置的值为'change'、'blur、数组,也可以不设置trigger属性。
trigger属性在async-validator中是不存在的,这个属性是element-ui添加的。
// element-ui\packages\form\src\form-item.vue
addValidateEvents() {
const rules = this.getRules();
if (rules.length || this.required !== undefined) {
// 监听blur和change事件
this.$on('el.form.blur', this.onFieldBlur);
this.$on('el.form.change', this.onFieldChange);
}
},
onFieldBlur() {
// 触发blur校验
this.validate('blur');
},
onFieldChange() {
if (this.validateDisabled) {
this.validateDisabled = false;
return;
}
// 触发change校验
this.validate('change');
},
el.form.blur 和 el.form.change 是在input/select等组件中dispatch出来的
2.3 过滤trigger后的规则
既然设置了trigger属性,那么在校验时应该要过滤对应trigger的规则,用过滤后的规则进行校验。
// element-ui\packages\form\src\form-item.vue
getFilteredRule(trigger) {
// 获取当前表单项的规则
const rules = this.getRules();
return rules.filter(rule => {
// 规则中未设置trigger或者当前的trigger为''时,规则会被用于校验
if (!rule.trigger || trigger === '') return true;
if (Array.isArray(rule.trigger)) {
// 规则中的trigger是数组的情况,需要包含当前的trigger
return rule.trigger.indexOf(trigger) > -1;
} else {
// 规则中的trigger与当前trigger完全匹配
return rule.trigger === trigger;
}
}).map(rule => objectAssign({}, rule));
}
从以上代码可以分析出:
-
规则中没有设置trigger,规则会被选取
-
当前查询的trigger为''时,规则会被选取
-
规则中设置了trigger,且当前查询的trigger不为''时,需要规则中的trigger包含当前trigger或者与之完全一致才会被选取。
2.4 表单项校验的值filedValue
上面分析了获得校验规则的代码,接下里是获取参与校验的值
// element-ui\packages\form\src\form-item.vue
fieldValue() {
// form表单的model属性
const model = this.form.model;
// 如果表单没有model属性或者表单项没有prop属性,则返回undefined
if (!model || !this.prop) { return; }
let path = this.prop;
if (path.indexOf(':') !== -1) {
path = path.replace(/:/, '.');
}
// 根据prop从model中读取值
return getPropByPath(model, path, true).v;
}
从代码中可以分析出:
-
表单form必须有model属性,如果没有model属性,参与校验的值就会是undefined;
-
表单项form-item必须有prop属性(这一点在获取校验规则时一致),如果没有prop属性,参与校验的值就会是undefined;
-
参与校验的值是读取表单model中prop对应的值
2.5 表单项校验validate
经过以上步骤,已经获取到表单项对应的规则以及参与校验的值,接下来就是进行校验
// element-ui\packages\form\src\form-item.vue
validate(trigger, callback = noop) {
this.validateDisabled = false;
// 拿到表单项的校验规则(过滤trigger之后的)
const rules = this.getFilteredRule(trigger);
// 如果没有匹配的校验规则,直接返回
if ((!rules || rules.length === 0) && this.required === undefined) {
callback();
return true;
}
this.validateState = 'validating';
const descriptor = {};
// 删除掉校验规则中的trigger属性,因为async-validator不需要
if (rules && rules.length > 0) {
rules.forEach(rule => {
delete rule.trigger;
});
}
descriptor[this.prop] = rules;
const validator = new AsyncValidator(descriptor);
const model = {};
// 参与校验的值
model[this.prop] = this.fieldValue;
validator.validate(model,
{ firstFields: true }, // 规则串行校验,遇到错误则停止
(errors, invalidFields) => { // 回调,校验完成后执行
this.validateState = !errors ? 'success' : 'error';
this.validateMessage = errors ? errors[0].message : '';
callback(this.validateMessage, invalidFields);
// 任一表单项校验时发射validate事件
this.elForm && this.elForm.$emit('validate', this.prop, !errors, this.validateMessage || null);
});
},
表单项的validate方法很清晰,就是获取表单项的规则,然后构造async-validator进行校验,在校验完成的回调中处理表单项的校验状态、执行传入的callback、发射validate事件。
三、表单Form
第二部分中我们弄清楚了表单项是如何进行校验的,这一部分我们分析一下Form是如何组织表单项进行校验的。
3.1 fields数组
form作为父组件,其中可以包含任意多个form-item组件,因此就有必要维护一个数据结构表示其所包含的form-item,在代码中使用的是fileds数组
// element-ui\packages\form\src\form-item.vue
mounted() {
if (this.prop) {
// 向'ElForm'组件发布‘el.form.addField’消息
this.dispatch('ElForm', 'el.form.addField', [this]);
......
}
},
beforeDestroy() {
// 向'ElForm'组件发布‘el.form.removeField’消息
this.dispatch('ElForm', 'el.form.removeField', [this]);
}
// element-ui\packages\form\src\form.vue
created() {
// 监听el.form.addField事件,填充fields数组
this.$on('el.form.addField', (field) => {
if (field) {
this.fields.push(field);
}
});
// 监听el.form.removeField事件,删除fields数组中对应的field
this.$on('el.form.removeField', (field) => {
if (field.prop) {
this.fields.splice(this.fields.indexOf(field), 1);
}
});
}
在form-item挂载时向form父组件发布el.form.addField消息,form组件接受消息后,将该form-item实例加入到fileds数组中。反之,在form-item销毁发送el.form.removeFileld消息,form组件接受消息后在fields中将该form-item实例删除。这样就维护了一个form-item的实例数组。
3.2 表单校验validate
上一步中已经维护了表单中的表单项的数组,那么对所有表单项进行校验就很简单了,我们来看代码
// element-ui\packages\form\src\form.vue
validate(callback) {
// 没有model直接返回
if (!this.model) {
console.warn('[Element Warn][Form]model is required for validate to work!');
return;
}
let promise;
if (typeof callback !== 'function' && window.Promise) {
promise = new window.Promise((resolve, reject) => {
callback = function(valid) {
valid ? resolve(valid) : reject(valid);
};
});
}
let valid = true;
let count = 0;
// 如果没有表单项,无需校验,直接返回true
if (this.fields.length === 0 && callback) {
callback(true);
}
let invalidFields = {};
// 对每一个表单项,调用表单项自身的validate方法进行校验
this.fields.forEach(field => {
field.validate('', (message, field) => {
if (message) {
// 有一个表单项校验失败,valid置未false
valid = false;
}
// 记录校验失败的表单项
invalidFields = objectAssign({}, invalidFields, field);
if (typeof callback === 'function' && ++count === this.fields.length) {
// 完成最后一个表单项的校验后,调用callback
callback(valid, invalidFields);
}
});
});
if (promise) {
// 如果没有传入callback,则返回的是一个promise对象
return promise;
}
}
从代码中可以看出,表单的校验确实就是依次执行表单项的校验,最后将校验结果组合后传入callback中执行。
至此,表单校验的源码就分析完成了,希望大家看了之后能有所收获^_^。