• 前言
  • element-ui中Form组件的简单使用
  • 源码实现需求分析
  • 校验方法
  • el-input组件实现
  • el-form-item组件实现
  • el-form组件实现
  • 组件实现中遗留的问题
  • 源码解析
  • 参考文献

手写el-form表单组件,再也不怕面试官问我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:true, message:'用户名不能为空' },
userPassword: { required:true, message:'密码不能为空' }
}
}
},
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: {
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混入的方式,用这个方法对上面代码组件代码进行改造,能够解决查找父元素子元素的问题数

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

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

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

源码解析

v-mode​l, ​rules​ 和 ​ref

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: [
{ required: true, message: '请输入活动名称', trigger: 'blur', validator: function() {} },
{ min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur', validator: function() {} }
],
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 {
elFormItem: this
};
},
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, { firstFields: true }, (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
};
};

参考文献

​​

​​

​​

欢迎关注《前端阳光》