文章目录

  • 场景
  • 思考
  • Mixin
  • 具体代码
  • 目录结构
  • validator相关代码
  • FormItem相关
  • 按需引用
  • 使用
  • 总结



场景

ElementUIForm表单组件自带的校验规则是不是有点少,通过yarn.lock查询ElementUI得知校验使用了async-validator依赖

vue elementui表单文件上传必填项校验_mixins

阅读async-validator相关文档得知内置类型如下:

  • string: 必须是 string,默认类型;
  • number: 必须是 number;
  • boolean: 必须是 boolean;
  • method: 必须是 function;
  • regexp: 必须是正则或者是在调用 new RegExp 时不报错的字符串;
  • integer: 必须是number类型且为整数;
  • float: 必须是number类型且为浮点数;
  • array: 必须是数组,通过 Array.isArray 判断;
  • object: 必须是对象且不为数组;
  • enum: 值必须出现在 enum 枚举值中;
  • date: 合法的日期,使用 Date 对象判断;
  • url: url类型;
  • hex: 16进制;
  • email: 邮箱地址;

常用类型有email,却没phone,如何自定义规则类型呢,比如ipmacphone,且达到同样的效果。

写法如下:

computed: {
   rules() {
     const required = true;
     return {
       email: [{ required, type: 'email', message: '请输入正确的邮箱' }],
       phone: [{ required, type: 'phone', message: '请输入正确的手机号码' }],
       url: [{ required, type: 'url', message: '请输入正确的url' }],
       ip: [{ required, type: 'ip', message: '请输入正确的IP地址' }],
       mac: [{ required, type: 'mac', message: '请输入正确的MAC地址' }]
     }
   }
 },

实现效果如下:(左:失败、右:成功)



vue elementui表单文件上传必填项校验_elementui_02


思考

通过阅读ElementUIFormItem组件源码,FormItem组件validate函数引用了async-validator,具体到下面29行代码,它初始化了一个校验器,若我们在初始化之前改造AsyncValidator函数是不是就能实现自定义类型扩展?
FormItem核心源码解读:

// 路径 node_modules\element-ui\packages\form\src\form-item.vue
import AsyncValidator from 'async-validator';
export default {
  methods: {
    // 核心校验函数 trigger值为change、blur 默认change
    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';
		// 以下代码可参考async-validator使用文档
        const descriptor = {};
        // 因为async-validator的rules没有trigger属性 删除每条规则的trigger 
        if (rules && rules.length > 0) {
          rules.forEach(rule => {
            delete rule.trigger;
          });
        }
        // 以FormItem当前prop属性为键赋值对应rules
        descriptor[this.prop] = rules;
		// 以descriptor为构造参数 初始化一个校验器
        const validator = new AsyncValidator(descriptor);
        const model = {};
		// 获取当前FormItem的对应字段值
        model[this.prop] = this.fieldValue;
		// 触发表单校验 firstFields: true 指当某一规则校验失败时,终止校验;
        validator.validate(model, { firstFields: true }, (errors, invalidFields) => {
          this.validateState = !errors ? 'success' : 'error';
          this.validateMessage = errors ? errors[0].message : '';
		  // 回调校验信息、不符合的字段
          callback(this.validateMessage, invalidFields);
          // 对应 Form Events 如果elForm存在,则每次触发validate事件
          // 参数:被校验的表单项 prop 值,校验是否通过,错误消息(如果存在)
          this.elForm && this.elForm.$emit('validate', this.prop, !errors, this.validateMessage || null);
        });
      },
  }
}

通过阅读asycn-validator官方文档,使用例子如下,是不是很熟悉,FormItem组件validate函数照着官方例子进一步封装,下面的Schema也就是上面的AsyncValidator,只要弄懂Schema大概便能实现我们要的效果。

import Schema from 'async-validator';

const descriptor = {
  // name字段的校验规则
  name: {type: "string", required: true},
};

const validator = new Schema(descriptor);

validator.validate({ name: "muji" }, (errors, fields) => {
  if (errors) {
    // 校验不通过 do something
    return;
  }
  // 校验通过 do something
});

通过阅读asycn-validator源码,Schema类开放了一个register注册器,不过官方文档并没有这个API的介绍,推测是注册校验规则类型。

// 路径node_modules\async-validator\es\index.js
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;
};

追溯到validators,这里存放了内置类型,验证了刚才的设想,我们在new AsyncValidator(descriptor)之前注册自己想要的类型,便可扩展。

vue elementui表单文件上传必填项校验_elementui_03


我们拎个默认类型string的源码观摩一下:

// node_modules\async-validator\es\validator\string.js
import rules from '../rule/';
import { isEmptyValue } from '../util';

/**
 *  对string类型校验.
 *
 *  @param rule 校验规则.
 *  @param value 源对象字段的值.
 *  @param callback 回调函数.
 *  @param source 校验后的源对象.
 *  @param options 校验选项.
 *  @param options.messages 校验信息.
 */
function string(rule, value, callback, source, options) {
  var errors = [];
  var validate = rule.required || !rule.required && source.hasOwnProperty(rule.field);
  if (validate) {
    // 如果表单值为空 且不是必填 不校验
    if (isEmptyValue(value, 'string') && !rule.required) {
      return callback();
    }
    // 校验必填
    rules.required(rule, value, source, errors, options, 'string');
    if (!isEmptyValue(value, 'string')) {
      // 以下代码为校验逻辑 可换成自定义业务校验
      // 校验值类型
      rules.type(rule, value, source, errors, options);
      // 校验是否满足区间
      rules.range(rule, value, source, errors, options);
      // 校验正则
      rules.pattern(rule, value, source, errors, options);
      if (rule.whitespace === true) {
        // 校验空白字符
        rules.whitespace(rule, value, source, errors, options);
      }
    }
  }
  callback(errors);
}

export default string;

如果我们扩展一个phone类型,就要实现类似上面这个string校验函数,
上面2636行代码可换成我们需要的校验函数,通过register函数注册,便注册了一个phone类型。
校验函数代码如下:

function validator(rule, value, callback, source, options) {
  const errors = [];
  // 如果表单值为空 且不是必填 不校验
  if (isEmptyValue(value, 'string') && !rule.required) {
    return callback();
  }
  // 校验必填
  rules.required(rule, value, source, errors, options);
  // 手机号校验
  if (!isEmptyValue(value, 'string') && !/^1\d{10}$/.test(value)) {
    errors.push('phone格式不正确');
  }
  return callback(errors);
}

上面1012代码是会根据业务变化的的,其余代码都是不变的,我们将校验业务抽出封装一下。
代码如下(上下对比一下):

function validator(rule, value, callback, source, options) {
  const type = rule.type;
  const errors = [];
  // 如果表单值为空 且不是必填 不校验
  if (isEmptyValue(value, 'string') && !rule.required) {
    return callback();
  }
  // 校验必填
  rules.required(rule, value, source, errors, options);
  // 自定义规则校验 ruleDict存放各种自定义的校验规则
  if (!isEmptyValue(value, 'string') && !ruleDict[type](value)) {
    errors.push(format('%s 格式不正确', rule.fullField));
  }
  return callback(errors);
}

ruleDict 存放各个类型的校验,比如portphone等等。
部分代码如下:

// ruleDict校验规则
export default {
  // 端口号
  port(value) {
    return /^[1-9][0-9]*$/.test(value) && parseInt(value, 10) <= 65535;
  },
  // 手机号
  phone(value) {
    return /^1\d{10}$/.test(value);
  }
};

统一进行register,并将 Schema吐出。

// 注册validator
Object.keys(ruleDict).forEach(key => {
  Schema.register(key, validator);
});
export default Schema;

Schema改造好了,那FormItem组件怎么引用呢?这是就想到了我们把源码整个copy一份不就好了?
这是一种办法,不过还有更好的,本文主角——混入(mixin);(总是最后出场)。

Mixin

混入 mixin是Vue自带的,用来复用Vue组件一种灵活方式。mixin对象可以包含任意组件选项,常见选项props、data、components、methods 、computed等等,还有常见的生命周期钩子比如created、mounted。
不仅mixin对象和组件之间互不影响,而且当组件和mixin对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
精简混入代码如下:

import { FormItem } from 'element-ui';
import { noop } from 'element-ui/src/utils/util';
// 引入扩展后的校验器
import AsyncValidator from './validator/index';

export default {
  name: 'ElFormItem',
  // 混入一个FormItem组件
  mixins: [FormItem],
  methods: {
    // 核心校验函数 trigger值为change、blur 默认change
    validate(trigger, callback = noop) {
      // 省略其余代码
      const validator = new AsyncValidator(descriptor);
      // 省略其余代码
    }
  }
};

如上代码,只要混入了FormItem组件,即可拥有其全部组件选项,即复制版FormItem组件,再重写methodsvalidate函数即可引入我们扩展后的AsyncValidator

这么实现的好处:

  1. 不用拷贝整个代码;
  2. 组件和复用组件之间互不影响,并以恰当方式合并;
  3. ElementUI组件升级,最多影响validate这一个函数;

最后再按需引用FormItem组件:

// 引入mixin后的FormItem组件
import FormItem from '@/components/Mixins/FormItem/index.js'
// 按需引用
Vue.component(FormItem.name, FormItem);

具体代码

目录结构

|-- components                  # 组件
        |   |-- Mixins                  # 混用组件
        |   |   |-- FormItem            
        |   |   |   |-- index.js        # mixin后的ElementUI FormItem组件
        |   |   |   |-- validator       # async-validator 校验器相关
        |   |   |       |-- index.js    # 注册自定义组件
        |   |   |       |-- rules.js    # 自定义校验规则

validator相关代码

// src\components\Mixins\FormItem\validator\rules.js
const patterns = {
  ip: /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/,
  mac: /^(([a-fA-F0-9]{2}[:-]?)){5}[a-fA-F0-9]{2}$/
};

export default {
  ip(value) {
    return patterns.ip.test(value);
  },
  port(value) {
    return /^[1-9][0-9]*$/.test(value) && parseInt(value, 10) <= 65535;
  },
  mac(value) {
    return patterns.mac.test(value);
  },
  phone(value) {
    return /^1\d{10}$/.test(value);
  }
};
// src\components\Mixins\FormItem\validator\index.js
import Schema from 'async-validator';
import rules from 'async-validator/lib/rule';
import { format, isEmptyValue } from 'async-validator/lib/util';
// 规则列表
import ruleList from './rules';

/**
 *  @param rule 校验规则.
 *  @param value 源对象字段的值.
 *  @param callback 回调函数.
 *  @param source 校验后的源对象.
 *  @param options 校验选项.
 *  @param options.messages 校验信息.
 */
function validator(rule, value, callback, source, options) {
  const type = rule.type;
  const errors = [];
  // 如果表单值为空 且不是必填 不校验
  if (isEmptyValue(value, 'string') && !rule.required) {
    return callback();
  }
  // 校验必填
  rules.required(rule, value, source, errors, options);
  // 自定义规则校验
  if (!isEmptyValue(value, 'string') && !ruleList[type](value)) {
    errors.push(format('%s 格式不正确', rule.fullField));
  }
  return callback(errors);
}

// 注册validator
Object.keys(ruleList).forEach(key => {
  Schema.register(key, validator);
});

export default Schema;

FormItem相关

// src\components\Mixins\FormItem\index.js
import { FormItem } from 'element-ui';
import { noop } from 'element-ui/src/utils/util';
import AsyncValidator from './validator/index';

export default {
  name: 'ElFormItem',
  mixins: [FormItem],
  methods: {
    // 核心校验函数 trigger值为change、blur 默认change
    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';
      // 以下代码可参考async-validator使用文档
      const descriptor = {};
      // 因为async-validator的rules没有trigger属性 删除每条规则的trigger
      if (rules && rules.length > 0) {
        rules.forEach(rule => {
          delete rule.trigger;
        });
      }
      // 以FormItem当前prop属性为键赋值对应rules
      descriptor[this.prop] = rules;
      // 以descriptor为构造参数 初始化一个校验器
      const validator = new AsyncValidator(descriptor);
      const model = {};
      // 获取当前FormItem的对应字段值
      model[this.prop] = this.fieldValue;
      // 触发表单校验 firstFields: true 指当某一规则校验失败时,终止校验;
      validator.validate(model, { firstFields: true }, (errors, invalidFields) => {
        this.validateState = !errors ? 'success' : 'error';
        this.validateMessage = errors ? errors[0].message : '';
        // 回调校验信息、不符合的字段
        callback(this.validateMessage, invalidFields);
        // 对应 Form Events 如果elForm存在,则每次触发validate事件
        // 参数:被校验的表单项 prop 值,校验是否通过,错误消息(如果存在)
        this.elForm && this.elForm.$emit('validate', this.prop, !errors, this.validateMessage || null);
      });
    }
  }
};

按需引用

在main.js里按需引用

// 引入mixin后的FormItem组件
import FormItem from '@/components/Mixins/FormItem/index.js'
// 按需引用
Vue.component(FormItem.name, FormItem);

使用

写法还是和原来一样如下:

<template>
	<el-form ref="form" :model="form" :rules="rules" label-width="120px">
	  <el-form-item label="ip" prop="ip">
	    <el-input v-model="form.ip" />
	  </el-form-item>
	  <el-form-item label="MAC" prop="mac">
	    <el-input v-model="form.mac" />
	  </el-form-item>
	</el-form>
</template>
<script>
export default {
  data() {
    return {
      form: {}
    }
  },
  computed: {
    rules() {
      const required = true;
      return {
        ip: [{ required, type: 'ip', message: '请输入正确的IP地址', trigger: 'blur' }],
        mac: [{ required, type: 'mac', message: '请输入正确的MAC地址', trigger: 'blur' }]
      }
    }
  },
}
</script>

写法还是和原来一样,却多了自定义校验规则,岂不美哉。

总结

通过源码解读,一步一步摸索出来的,新人创作不易,求点赞关注给点动力。