手写el-form表单组件,再也不怕面试官问我form表单原理

前端阳光

共 34144字,需浏览 69分钟

 ·

2021-08-22 08:56


  • 前言

  • element-ui中Form组件的简单使用

  • 源码实现需求分析

  • 校验方法

  • el-input组件实现

  • el-form-item组件实现

  • el-form组件实现

  • 组件实现中遗留的问题

  • 源码解析

  • 参考文献

前言

最近在用elementUI的form表单组件的时候,在实现嵌套表单的校验的时候,遇到了一些困难,我想之所以困难的原因在于我对elementui里的form表单组件不够熟悉,于是就深入了解了一下源码,并尝试自己去实现一个自己的form表单

欢迎关注《前端阳光》,加入技术交流群,加入内推群

element-ui中Form组件的简单使用

<template>
  <el-form :model="info" :rules="rules" ref="forms" > 
      <el-form-item label="用户名:" prop="userName"> 
          <el-input v-model="info.userName" placeholder="请输入用户名"></el-input>  
      </el-form-item>  
      <el-form-item> 
          <el-input type="password" v-model="info.userPassword" placeholder="请输入密码"></el-input>  
      </el-form-item>
      <el-form-item> 
          <button @click="save">提交</button>
      </el-form-item>  
  </el-form>
<template\>
    
<script>
  data() {
   return {
     info: {
         userName:'',
         userPassword:''
     },
     rules: {
         userName: { required:truemessage:'用户名不能为空' },
         userPassword: { required:truemessage:'密码不能为空' }
    }
   }
  },
  methods: {
    save() {
      this.$refs.forms.validate((result) => {
          let message ='校验经过';
          if (!result) {
              message ='校验未经过';
          }
          alert(message)
    }
  }
</script>

首先要清楚一下组件的使用方式

el-form接收model和rule两个prop

  • model表示表单绑定的数据对象
  • rule表示验证规则策略,表单验证

el-form-item接收的prop属性,对应form组件的model数据中某个key值,如果rule刚好有key,给定的条件下去如失去焦点验证规则匹不匹配。

也就是el-form-item要获得model[prop]和rule[prop]两个值,检查 model[prop]是否符合rule[prop]设置的规则。

源码实现需求分析

实现一个el-form组件,其中接受model与rules两个props,而且开放一个验证方法validate,用于外部调用,验证组件内容

实现一个el-form-item组件,其中接受label与prop两个props。且在这里要注意的是el-form-item能够做为中间层,链接el-form与el-form-item中的的slot,并进行核心的验证处理,因此数据验证部分在这个组件中进行。

实现一个el-input组件,实现双向绑定,其中接受value与type两个props

好了,分析完基本需求以后,下面咱们开干。npm

校验方法

咱们这里使用一个对数据进行异步校验的库async-validator,element-ui中也是使用的这个库。

el-input组件实现

input组件中须要实现双向绑定以及向上层el-form-item传递数据和通知验证。

// 双向绑定的input本质上实现了input而且接收一个value
// 这里涉及到的vue组件通讯为$attrs,接受绑定传入的其余参数,如placeholder等
<template>
    <input :type="type" :value="value" @input="onInput" v-bind="$attrs" />
</template>

<script>
    // 这里涉及到的vue组件通讯为provide/inject
    export default {
        props: {
            value: {
                type: String,
                default: ‘’,
            },
            type: {
                type: String,
                default: 'text'
            }
        },
    },
    methods: {
        onInput(e) {
            this.$emit('input', e.target.value);
            // 通知父元素进行校验 使用this.$parent找到父元素el-form-item
            this.$parent.$emit('validate');
        }
    }
</script>

el-form-item组件实现

el-form-item组件做为数据验证中间件,要接受el-form中的数据,结合el-input中的数据根据el-form中的rules进行验证,并进行错误提示

<template>
    <div>
        <label v-text="label"></label>
        <slot></slot>
        <p v-if="error" v-text="error"></p>
    </div>
</template>

<script>
    // 引入异步校验数据的库
    import Schema from 'async-validator';
    // 这里涉及到的vue组件通讯为provide/inject
    export default {
        // 接收el-form组件的实例,方便调用其中的属性和方法
        inject: ['form'],
        props: {
            label: {
                typeString,
                default'',
            },
            prop: {
                typeString,
                requiredtrue,
                default''
            }
        },
    },
    data() {
        return {
            // 错误信息提示
            error:''
        }
    },
     

    mounted(){
        // 监听校验事件
        this.$on('validate', () => { this.validate() })
    },
    methods: {
        // 调用此方法会进行数据验证,并返回一个promise
        validate() {
            // this.prop为验证字段,如: userName
            // 获取验证数据value,如: userName的值
            const value = this.form.model[this.prop];
            
            // 获取验证数据方法,如: { required:true, message:'用户名不能为空' }
            const rules = this.form.rules[this.prop];
            
            // 拼接验证规则
            const desc= { [this.prop]: rules };
            // 实例化验证库
            const schema = new Schema(desc);
            
            // 这里会返回一个promise
            return schema.validate(
                { [this.prop]: value }, 
                errors => {
                    if (errors) {
                        this.error = errors[0].message;
                    } else {
                        this.error = '';
                    }
                }
            )
        }
    }
</script>

el-form组件实现

咱们上面分析过el-form只须要接受props值,并开放一个验证方法validate判断校验结果,而后把内嵌的slot内容展现出来,那么el-form实现就相对简单了

<template>
    <div>
        <slot></slot>
    </div>
</template>

<script>
// 这里涉及到的vue组件通讯为provide/inject
export default {
    // 由于上面需求分析提到,须要在form-item组件中进行验证,因此要将form实例总体传入form-item中,方便后续调用其方法和属性
    provide() {
        return {
            formthis
        }
    },
    props: {
        model: {
            type:Object,
            required:true,
            default() => ({}),
        },
        rules: {
            type:Object,
            default() => ({})
        }
    },
},
methods: {
    // 这是供外部调用的validate验证方法 接收一个回调函数 把验证结果返回出去
    validate(callback) {
        // 使用this.$children找到全部el-form-item子组件,获得的值为一个数组,并调用子组件中的validate方法并获得Promise数组 
        const tasks = this.$children
         .filter(item => item.prop) 
         .map(item => item.validate());  
        // 全部任务必须所有成功才算校验经过,任一失败则校验失败 
        Promise.all(tasks)
         .then(() => callback(true))
         .catch(() => callback(false))
    }
}
</script>

到这里Form组件的构建基本就结束了,这里涉及到的Vue组件通讯有不少,学习这部分源码能很大程度上的帮助咱们理解Vue中组件通讯的机制以及提高咱们的编程能力。

组件实现中遗留的问题

  • 实现到这步其实还不能彻底放心,这个组件还不够健壮。由于在组件源码中还有一些处理在这里尚未提到。
  • 若是在el-form组件中嵌套层级很深的话this.$children可能拿到的并非el-form-item,一样el-input的this.$parent拿到的也不是el-form-item,那这个问题要怎么处理呢?
  • 其实在vue 1.x中提供了两个方法全局方法dispatch和boardcast,他能够根据指定componentName来递归查找相应组件并派发事件,在vue 2.x中这个方法被废弃了。可是element-ui以为这个方法有用,因而又把他实现了一遍,而且在解决上面这个问题中就可使用到,具体源码以下:
const boardcast = function (componentName, eventName, params{
    this.$children.forEach(child => {
        let name = child.$options.componentName;
        if (componentName === name) {
            child.$emit.apply(child, [eventName].concat(params));
        } else {
            boardcast.apply(child, [componentName, eventName].concat(params));
        }
    });
}

export default {
    methods: {
        // 向上寻找父级元素
        dispatch(componentName, eventName, params) {
            let parent = this.$parent || this.$root;
            let name = parent.$options.componentName;

            while (parent && (!name || name !== componentName)) {
                parent = parent.$parent;
                if (parent) {
                    name = parent.$options.componentName;
                }
            }

            if (parent) {
                parent.$emit.apply(parent, [eventName].concat(params));

             }
        },

        // 向下寻找子级元素
        boardcast(componentName, eventName, params) {
            boardcast.call(this, componentName, eventName, params);
        }
    }

};

使用mixin混入的方式,用这个方法对上面代码组件代码进行改造,能够解决查找父元素子元素的问题数

到这里,实际上已经完成一个基本的表单了,当然,element的表单功能是比这个强大得多的,例如el-form-item要获得rule不止可以在el-form中获取,也可以通过直接以props的方式传入el-form-item,这时候,props获得的rule就会覆盖掉el-form的rule。

为了更全面的了解element的el-form表单是怎么实现的,为了提高我们的编程能力,建议看看el-form的源码解析。

欢迎关注《前端阳光》,加入技术交流群,加入内推群

源码解析

v-model, rulesref

v-model 配合 prop 使用,对应的是要校验字段的值(prop 一定是在 el-form-itme上面,在源码部分会解释为什么)

rule 和 prop 对应,指的是每个字段的校验规则(在源码部分会解释为什么)

ref 最后一步校验使用,和v-model 对应

<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px">
  <el-form-item label="活动名称" prop="name">
    <el-input v-model="ruleForm.name"></el-input>
  </el-form-item>
 <el-form-item label="年龄" prop="age">
    <el-input v-model.number="ruleForm.age"></el-input>
  </el-form-item>
  <el-form-item>
    <el-button type="primary" @click="submitForm('ruleForm')">立即创建</el-button>
    <el-button @click="resetForm('ruleForm')">重置</el-button>
  </el-form-item>
</el-form>
<script>
  var checkAge = (rule, value, callback) => {
  
  
   // rule => { validator: '',  field: "score.0", fullField: "score.0", type: "string", max_age: '' ...}
    // field 是 对应props里面的值
    // validator 是async-validator 里面的 validator(description)
    // value 要校验的值
    //console.log(rule.max_age)
    
    
    
    if (!value) {
       return callback(new Error('年龄不能为空'));
     }
     if (!Number.isInteger(value)) {
         callback(new Error('请输入数字值'));
      } else {
        if (value < rule.max_age) {
          callback(new Error('必须年满18岁'));
        } else {
          callback();
        }
      }
  };
  export default {
    data() {
      return {
        ruleForm: {
          name'',
          age:''
        },
        rules: {
          name: [
            { requiredtruemessage'请输入活动名称'trigger'blur'validatorfunction({} },
            { min3max5message'长度在 3 到 5 个字符'trigger'blur'validatorfunction({} }
          ],
          age: [
            {max_age:18, validator: checkAge, trigger'blur' }// checkAge自定义规则函数
          ]
        }
      };
    },
    methods: {
      submitForm(formName) {
        this.$refs[formName].validate((valid, form) => {
        
        // form 里面是校验没通过的prop
        
          if (valid) {
            alert('submit!');
          } else {
            console.log('error submit!!')
          }
        });
      },
      resetForm(formName) {
        this.$refs[formName].resetFields();
      }
    }
  }
</script>

也可以将校验规则写在form上面

<el-form :model="numberValidateForm" ref="numberValidateForm" label-width="100px" class="demo-ruleForm">
  <el-form-item
    label="年龄"
    prop="age"
    :rules="[
      { required: true, message: '年龄不能为空'},
      { type: 'number', message: '年龄必须为数字值'}
    ]"
>

    <el-input type="age" v-model.number="numberValidateForm.age" autocomplete="off"></el-input>
  </el-form-item>

以至于循环使用也是没有问题的

<el-form>
  <el-form-item
    v-for="(domain, index) in dynamicValidateForm.domains"
    :label="'域名' + index"
    :key="domain.key"
    :prop="`domains.${index}.value`" //绑定的prop
    :rules="[
     { required: true, message: '域名不能为空', trigger: 'blur' },
     {reg:/^--------$/, validator: checkDomain, trigger: 'blur' }
    ]"

  >

</el-form-item>

然后来分析一波源码

// form.vue
//#76 form-item会emit一个事件,接收就好
created() {
    this.$on('el.form.addField', (field) => {
      if (field) {
        this.fields.push(field);
      }
    });
    /* istanbul ignore next */
    this.$on('el.form.removeField', (field) => {
      if (field.prop) {
        this.fields.splice(this.fields.indexOf(field), 1);
      }
    });
  },

// # 109 
// 我们使用的this.$refs['formname'].validate 里面的validate 就是这个validate
validate(callback) {
    if (!this.model) { // 如果没有模板直接报错
      console.warn('[Element Warn][Form]model is required for validate to work!');
      return;
    }
    let promise;
    // if no callback, return promise
    if (typeof callback !== 'function' && window.Promise) {
      promise = new window.Promise((resolve, reject) => {
        callback = function(valid// 这个valid是从form-item 里面返回的,下面会讲
          valid ? resolve(valid) : reject(valid);
        };
      });
    }
    let valid = true;
    let count = 0;
    // 如果需要验证的fields为空,调用验证时立刻返回callback
    if (this.fields.length === 0 && callback) {
      callback(true);
    }
    let invalidFields = {};
    this.fields.forEach(field => { // 猜测这个field应该是一个form-item 的示例
      field.validate('', (message, field) => { // 这个validate也是form-item里面的
        if (message) {
          valid = false// 存在校验没通过
        }
        invalidFields = objectAssign({}, invalidFields, field); // 应该是重新了Object.assign
        if (typeof callback === 'function' && ++count === this.fields.length) { // 最后一个的处理
          callback(valid, invalidFields); // 如果cb是函数,正常执行,参数是校验结果和校验失败的field
        }
      });
    });
    if (promise) {
      return promise;// 如果没有cb,那么返回一个promise,如果有promise返回一个promise, 这样写提高兼容性
    }
  },


// form-item.vue ,这里主要讲几个关键的方法
// # 54
  provide() {
    return {
      elFormItemthis
    };
  }, 
  inject: ['elForm'],
// 对内注入elForm, 对外抛出elFormItem

//#189 每个form-item 单独校验
import AsyncValidator from 'async-validator';
  validate(trigger, callback = noop) { // 这个就是我上文提到的form-item 里面的validate
      this.validateDisabled = false;
      const rules = this.getFilteredRule(trigger); //获取rules
      if ((!rules || rules.length === 0) && this.required === undefined) { // 没有rules, 直接通过
        callback();
        return true
      }
      this.validateState = 'validating';
      const descriptor = {};
      if (rules && rules.length > 0) {
        rules.forEach(rule => {
          delete rule.trigger;
        });
      }
      descriptor[this.prop] = rules; //每个form-item 单独校验
      const validator = new AsyncValidator(descriptor);
      const model = {};
      model[this.prop] = this.fieldValue; // 这里就是为什么一定要有model, 而且props必须可以直接访问
      validator.validate(model, { firstFieldstrue }, (errors, invalidFields) => { // 参考asyn-validator 不展开
      // validation failed, errors is an array of all errors
      // fields is an object keyed by field name with an array of
      // errors per field
      // https://github.com/yiminghe/async-validator
      //- firstFields: Boolean|String[], Invoke callback when the first validation rule of the specified sofield generates an error, no more validation rules of the same field are processed. true means all fields. 所以项,只要有规则一个产生error, 该项后面规则都不执行。
      //说到底就是对async-validator一个封装
        this.validateState = !errors ? 'success' : 'error';
        this.validateMessage = errors ? errors[0].message : '';
        callback(this.validateMessage, invalidFields);
        this.elForm && this.elForm.$emit('validate'this.prop, !errors, this.validateMessage || null);
      });
    },


//# 252
 getRules() {
    let formRules = this.form.rules; // parent 组件的rules
    const selfRules = this.rules; // prop 传入的rules
    const requiredRule = this.required !== undefined ? { required: !!this.required } : []; // prop 传入
    const prop = getPropByPath(formRules, this.prop || ''); // 判断parent 传的rules是否和prop传入冲突
    formRules = formRules ? (prop.o[this.prop || ''] || prop.v) : []; // 以prop 为准
    return [].concat(selfRules || formRules || []).concat(requiredRule); // 整合
  },
  getFilteredRule(trigger) {
    const rules = this.getRules();
    return rules.filter(rule => {
      if (!rule.trigger || trigger === ''return true// 全触发
      if (Array.isArray(rule.trigger)) {
        return rule.trigger.indexOf(trigger) > -1//按需触发
      } else {
        return rule.trigger === trigger;
      }
    }).map(rule => objectAssign({}, rule));
  },

// #130
  form() { // 找到parent为el-form的组件
    let parent = this.$parent;
    let parentName = parent.$options.componentName;
    while (parentName !== 'ElForm') {
      if (parentName === 'ElFormItem') {
        this.isNested = true;
      }
      parent = parent.$parent;
      parentName = parent.$options.componentName;
    }
    return parent;
  },

// util #47
export function getPropByPath(obj, path, strict{
  let tempObj = obj;
  path = path.replace(/\[(\w+)\]/g'.$1'); // enleve []
  path = path.replace(/^\./''); // enleve first.

  let keyArr = path.split('.');
  let i = 0;
  for (let len = keyArr.length; i < len - 1; ++i) {
    if (!tempObj && !strict) break;
    let key = keyArr[i];
    if (key in tempObj) {
      tempObj = tempObj[key]; // neste address
    } else {
      if (strict) {
        throw new Error('please transfer a valid prop path to form item!');
      }
      break;
    }
  }
  return {
    o: tempObj,
    k: keyArr[i],
    v: tempObj ? tempObj[keyArr[i]] : null
  };
};

参考文献

https://www.shangmayuan.com/a/1481921e712d4136b6edf6a9.html

https://juejin.cn/post/6870015646520836109#comment

欢迎关注《前端阳光》,加入技术交流群,加入内推群


浏览 95
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报