揭秘element-ui表单校验

4,709 阅读7分钟

​ 表单是前端开发中经常使用的组件,在表单使用的过程中不可避免地会遇到表单校验的场景。

​ 在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));
}

​ 从以上代码可以分析出:

  1. 规则中没有设置trigger,规则会被选取

  2. 当前查询的trigger为''时,规则会被选取

  3. 规则中设置了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;
}

​ 从代码中可以分析出:

  1. 表单form必须有model属性,如果没有model属性,参与校验的值就会是undefined;

  2. 表单项form-item必须有prop属性(这一点在获取校验规则时一致),如果没有prop属性,参与校验的值就会是undefined;

  3. 参与校验的值是读取表单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中执行。

至此,表单校验的源码就分析完成了,希望大家看了之后能有所收获^_^。