手撸element-ui的Form组件一窥vue组件间通讯

在Vue项目的开发过程当中,Vue与element-ui能够说是项目开发标配。在学习Vue的时候,结合element-ui源码食用更佳。下面我会经过手撸element-ui中的Form组件,深刻分析Vue组件中的通讯方式。vue

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:true, message:'用户名不能为空' },
               userPassword: { required:true, message:'密码不能为空' }
           }
       }
    },
    methods: {
        save() {
            this.$refs.forms.validate((result) => {
                let message ='校验经过';
                if (!result) {
                    message ='校验未经过';
                }
                alert(message)
        }
    }
</script>

这是一个简单的用户名和密码的验证,在这里面,使用了如下的属性:git

Form Attributes

参数 类型 说明
model object 表单数据对象
rules object 表单验证规则

Form Methods

参数 类型 说明
validate Function(callback: Function(boolean, object)) 对整个表单进行校验的方法,参数为一个回调函数。该回调函数会在校验结束后被调用,并传入两个参数:是否校验成功和未经过校验的字段。若不传入回调函数,则会返回一个 promise

Form-Item Attributes

参数 类型 说明
label string 标签文本
prop string 表单域 model 字段,在使用 validate、resetFields 方法的状况下,该属性是必填的

此次就经过这个例子展开分析,从源码级别分析分析该组件实现过程。github

源码实现需求分析

  • 实现一个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: {
                type: String,
                default: '',
            },
            prop: {
                type: String,
                required: true,
                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 {
                form: this
            }
        },
        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中组件通讯的机制以及提高咱们的编程能力。element-ui

组件实现中遗留的问题

  • 实现到这步其实还不能彻底放心,这个组件还不够健壮。由于在组件源码中还有一些处理在这里尚未提到。
  • 若是在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混入的方式,用这个方法对上面代码组件代码进行改造,能够解决查找父元素子元素的问题数组

写在最后

通过改造后的完整代码在个人Github上,有须要的同窗能够去看一下,若是对你有帮助,欢迎star。
第一次写文章,里面可能还有不少问题须要改进,欢迎你们指正。
个人公众号是....忘了我没有公众号,hhhhhh。
最近在研究Vue源码,过年时间比较多,会陆续写一写Vue源码相关文章,好比Vuex,Vue-router的分析和简单实现。你们一块儿学习,一块儿进步。promise

相关文章
相关标签/搜索