在Vue项目的开发过程中,Vue与element-ui可以说是项目开发标配。在学习Vue的时候,结合element-ui源码食用更佳。下面我会通过手撸element-ui中的Form组件,深入分析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>
这是一个简单的用户名和密码的验证,在这里面,使用了以下的属性:
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 方法的情况下,该属性是必填的 |
这次就通过这个例子展开分析,从源码级别分析分析该组件实现过程。
源码实现需求分析
- 实现一个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
好了,分析完基本需求之后,下面我们开干。
校验方法
我们这里使用一个对数据进行异步校验的库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中组件通信的机制以及提升我们的编程能力。
组件实现中遗留的问题
- 实现到这步其实还不能完全放心,这个组件还不够健壮。因为在组件源码中还有一些处理在这里还没有提到。
- 如果在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的分析和简单实现。大家一起学习,一起进步。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。