场景描述
vue2.x + elments 是目前大家非常常用的配方,中台开发的不错选择之一吧~
既然使用频率这么高,我们就有必要了解ElForm 这个组件的工作过程,实现原理;
上一篇《让我们浅浅的深入了解一下天天都在用的 el-form 组件(一)》 中我们从头到尾了解了el-form 的实现过程,从整个过程中我们了解到他的实现并不复杂,el-form 更多的是提供一个容器,去存放 el-form-item,
-
在 template 部分 非常简洁,就提供了一些动态class ,适配不同场景下的不同样式
-
在 computed 部分 提供了一些计算属性主要是用于计算 label-width 的相关数据
-
在 watcher 部分监听了 rules ,也就是我们提供的校验规则对象
-
在 created 的hooks 中,使用了 $on 监听了 两个事件 el-form.addField,el-form-removeField
-
在 data 函数中 声明了 fields(表单的所有字段集) 和 potentialLabelWidthArr (计算 autoWidht 的一个集合)
methods 中定义了 几个表单的相关API , 主要分为校验相关的函数,和计算label-width 相关的函数
今天我们通过上一篇遗留的如下的问题来分析解答每一个问题:
遗留疑问
我们先来看看遗留的问题列表:
-
created 的 监听的 field 的 add/remove 事件,在哪儿触发的,在什么时机触发的 ?
-
data 中定义的 fields 集合里面到底存放的是什么 ?
-
field 中的 validate 函数到底是如何实现的 ?
-
关于 lableWidth 的 这些函数方法在哪儿使用的 ?
从源码入手
el-form-item 组件的源码目录为:element/blob/dev/packages/form/src/form-item.vue ,快捷入口 点我
还是老规矩 先看 template 部分
vue
<template>
<div class="el-form-item" :class="[{
'el-form-item--feedback': elForm && elForm.statusIcon,
'is-error': validateState === 'error',
'is-validating': validateState === 'validating',
'is-success': validateState === 'success',
'is-required': isRequired || required,
'is-no-asterisk': elForm && elForm.hideRequiredAsterisk
},
sizeClass ? 'el-form-item--' + sizeClass : ''
]">
<label-wrap
:is-auto-width="labelStyle && labelStyle.width === 'auto'"
:update-all="form.labelWidth === 'auto'">
<label :for="labelFor" class="el-form-item__label" :style="labelStyle" v-if="label || $slots.label">
<slot name="label">{{label + form.labelSuffix}}</slot>
</label>
</label-wrap>
<div class="el-form-item__content" :style="contentStyle">
<slot></slot>
<transition name="el-zoom-in-top">
<slot
v-if="validateState === 'error' && showMessage && form.showMessage"
name="error"
:error="validateMessage">
<div
class="el-form-item__error"
:class="{
'el-form-item__error--inline': typeof inlineMessage === 'boolean'
? inlineMessage
: (elForm && elForm.inlineMessage || false)
}"
>
{{validateMessage}}
</div>
</slot>
</transition>
</div>
</div>
</template>
我们可以清晰地看到,使用了 class 为 el-form-item 的div 作为最外层容器,并且根据一系列的条件判断添加了很多动态的class 列表。
包含了 内容部分 包含了 label-wrap 和 div.el-form-item__content 两个部分,分别对应的就是 表单的 label, 和 内容部分。
label-wrap 我们稍后再去看他是怎么个实现
div.el-form-item__content 提供了 solt 用于放置具体的表单元素,如 input, checkbox ,select ,以及其他任意的内容;transition 部分就是错误信息的展示部分了,使用的el-zoom-in-top 的动画样式;
template 部分就这样简单的看完了,内容非常的简单 .
接下来我们继续 js 部分
import 部分
JavaScript
import AsyncValidator from 'async-validator'; // 一个表单校验框架
import emitter from 'element-ui/src/mixins/emitter'; // 事件
import objectAssign from 'element-ui/src/utils/merge';
import { noop, getPropByPath } from 'element-ui/src/utils/util';
import LabelWrap from './label-wrap';
async-validator 为一个表单校验工具库 见文档 el 正是使用了该库来完成了表单的校验工作
emitter :这里提供了跨层级的通讯能力,具体可以看这篇《浅析Element ui 中事件 broadcast 与 dispatch》
objectAssign 函数为一个工具函数,旨在对象的属性合并,具体和 Object.assign() 类似,有兴趣的可以查看一下他们的实现;
noop, getPropByPath 都是工具函数,noop:定义了一个空函数,getPropByPath 通过path 获取 prop
LabelWrap 就是 lebel 的包装组件
props等选项
yaml
name: 'ElFormItem',
componentName: 'ElFormItem',
mixins: [emitter],
provide() {
return {
elFormItem: this
};
},
inject: ['elForm'],
props: {
label: String,
labelWidth: String,
prop: String,
required: {
type: Boolean,
default: undefined
},
rules: [Object, Array],
error: String,
validateStatus: String,
for: String,
inlineMessage: {
type: [String, Boolean],
default: ''
},
showMessage: {
type: Boolean,
default: true
},
size: String
},
components: {
// use this component to calculate auto width
LabelWrap
},
这里我们简单的过一下,不作具体的赘述,因为意义不大,都是一些常规的神明以及固有的选项值等。
data() 部分
javascript
data() {
return {
validateState: '',
validateMessage: '',
validateDisabled: false,
validator: {},
isNested: false,
computedLabelWidth: ''
};
},
依然是声明了很多属性,更多的还是 validate相关的,以及 labelWidht 相关的
watch 部分
javascript
watch: {
error: {
immediate: true,
handler(value) {
this.validateMessage = value;
this.validateState = value ? 'error' : '';
}
},
validateStatus(value) {
this.validateState = value;
},
rules(value) {
if ((!value || value.length === 0) && this.required === undefined) {
this.clearValidate();
}
}
},
监听了 error 的变化,给validateMessage以及validateState 赋值,以及校验rules的变化来调用clearValidate清除字段的校验;依旧是非常的简单。
computed 部分
JavaScript
computed: {
labelFor() {
return this.for || this.prop;
},
// 计算 lebel 的 样式信息,用于 LabelWrap 组件的is-auto-witdh 属性判断
labelStyle() {
const ret = {};
if (this.form.labelPosition === 'top') return ret;
const labelWidth = this.labelWidth || this.form.labelWidth;
if (labelWidth) {
ret.width = labelWidth;
}
return ret;
},
// 计算 表单 内容元素的样式 主要是根据 leble的样式 计算不同场景下的 content 的 marginLeft 值
contentStyle() {
const ret = {}
const label = this.label;
if (this.form.labelPosition === 'top' || this.form.inline) return ret;
if (!label && !this.labelWidth && this.isNested) return ret;
const labelWidth = this.labelWidth || this.form.labelWidth;
if (labelWidth === 'auto') {
if (this.labelWidth === 'auto') {
ret.marginLeft = this.computedLabelWidth;
} else if (this.form.labelWidth === 'auto') {
ret.marginLeft = this.elForm.autoLabelWidth;
}
} else {
ret.marginLeft = labelWidth;
}
return ret;
},
// 通过不停的找爹模式 找到 el-form, 因为 el-form 是可以包裹任意元素的,不限层级,所以这个地方一直向上找爹,直到找到;
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;
},
// 获取 prop 的对应的model 的值;
fieldValue() {
const model = this.form.model;
if (!model || !this.prop) { return; }
let path = this.prop;
if (path.indexOf(':') !== -1) {
path = path.replace(/:/, '.');
}
return getPropByPath(model, path, true).v;
},
// 计算该表单元素是不需要必填, getRules 就是获取了 该表单项的所有校验规则,再查询这些规则中是否有 required 为真的项目,最终返回
isRequired() {
let rules = this.getRules();
let isRequired = false;
if (rules && rules.length) {
rules.every(rule => {
if (rule.required) {
isRequired = true;
return false;
}
return true;
});
}
return isRequired;
},
// 获取 表单的上设置的表单项的尺寸大小;对应的就是文档中的 mini,small 这个些个值
_formSize() {
return this.elForm.size;
},
// 获取 表单项的size,规则为优先自身设置的值其次再试表单统一设置的值
elFormItemSize() {
return this.size || this._formSize;
},
// ..... 不同size 对应的class
sizeClass() {
return this.elFormItemSize || (this.$ELEMENT || {}).size;
}
},
代码较长,我们直接在源码上添加注释,我们进行简单的备注,请看属性的备注信息;
methods 部分
JavaScript
// 表单项的校验函数 ,
validate(trigger, callback = noop) {
this.validateDisabled = false
// 首先根据 trigger 参数 获取了 该字段的校验规则
const rules = this.getFilteredRule(trigger)
// 如果校验规则没有,或者 都不需要必填,就直接执行 callback ,并返回 true
if ((!rules || rules.length === 0) && this.required === undefined) {
callback();
return true;
}
// 这里就开始了正在的校验逻辑了
// 先初始化了 validateState 为 正在校验,这个状态也对应了 template 中的 校验状态 class 样式信息。
this.validateState = 'validating';
// 申明校验的descriptor
const descriptor = {};
// 这里如果校验规则存在,就吧每一项的 trigger 属性删掉
if (rules && rules.length > 0) {
rules.forEach(rule => {
delete rule.trigger;
});
}
// 定义了 prop 的 rules 用于初始化 AsyncValidator 的实例
descriptor[this.prop] = rules;
// 初始化 AsyncValidator 的实例,validator
const validator = new AsyncValidator(descriptor);
// 吧 fieldValue 赋值给 model 的prop ,因为 validator.validate 的参数类型原因,申明生来了对象模式;
const model = {};
model[this.prop] = this.fieldValue;
// 调用 validator.validate 的方式,完成校验工作,包括 设定 validate 的状态,错误信息,以及执行 callback 注入 validateMessage ,invalidFields , 这也就是我们可以再 el-form 中收集到 invalidFields 的原因,因为这里传入了这些需要的信息
validator.validate(model, { firstFields: true }, (errors, invalidFields) => {
this.validateState = !errors ? 'success' : 'error';
this.validateMessage = errors ? errors[0].message : '';
callback(this.validateMessage, invalidFields);
// 去触发 form 的 validate 事件,并注入参数
this.elForm && this.elForm.$emit('validate', this.prop, !errors, this.validateMessage || null);
});
},
// 清楚Validate 重置一些信息
clearValidate() {
this.validateState = '';
this.validateMessage = '';
this.validateDisabled = false;
},
// 重置 field 的值,这就是 el-form 中调用 resetFields 中 循环调用 field的 resetField 函数的实现。处理不同场景下的 值等;
resetField() {
this.validateState = '';
this.validateMessage = '';
let model = this.form.model;
let value = this.fieldValue;
let path = this.prop;
if (path.indexOf(':') !== -1) {
path = path.replace(/:/, '.');
}
let prop = getPropByPath(model, path, true);
this.validateDisabled = true;
if (Array.isArray(value)) {
prop.o[prop.k] = [].concat(this.initialValue);
} else {
prop.o[prop.k] = this.initialValue;
}
// reset validateDisabled after onFieldChange triggered
this.$nextTick(() => {
this.validateDisabled = false;
});
this.broadcast('ElTimeSelect', 'fieldReset', this.initialValue);
},
// 获取 校验规则, 先获取了 form 上定义的规则,再获取自身定义的规则,以及 requiredRule 等,最后合并到一起,
getRules() {
let formRules = this.form.rules;
const selfRules = this.rules;
const requiredRule = this.required !== undefined ? { required: !!this.required } : [];
const prop = getPropByPath(formRules, this.prop || '');
formRules = formRules ? (prop.o[this.prop || ''] || prop.v) : [];
return [].concat(selfRules || formRules || []).concat(requiredRule);
},
// 工具函数 获取 通过 trigger 过滤后的规则;
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));
},
// 触发 Blur 事件,就调用 validate函数去触发校验。
onFieldBlur() {
this.validate('blur');
},
// 同上 触发 change 事件 , 就调用 validate函数去触发校验。
onFieldChange() {
if (this.validateDisabled) {
this.validateDisabled = false;
return;
}
this.validate('change');
},
// .... 顾名思义 ,哈哈哈
updateComputedLabelWidth(width) {
this.computedLabelWidth = width ? `${width}px` : '';
},
// 添加事件(blur , change )的监听,去执行对象校验函数
addValidateEvents() {
const rules = this.getRules();
if (rules.length || this.required !== undefined) {
this.$on('el.form.blur', this.onFieldBlur);
this.$on('el.form.change', this.onFieldChange);
}
},
// 移除事件监听。
removeValidateEvents() {
this.$off();
}
这部分代码也比较长,我们还是通过源码加备注的方式来理解,请看备注信息吧!
mounted 部分
kotlin
mounted() {
if (this.prop) {
this.dispatch('ElForm', 'el.form.addField', [this]);
let initialValue = this.fieldValue;
if (Array.isArray(initialValue)) {
initialValue = [].concat(initialValue);
}
Object.defineProperty(this, 'initialValue', {
value: initialValue
});
this.addValidateEvents();
}
},
这部分内容也是非常简单,就是判断如果设置了 prop 属性,在dom 渲染完成后触发 el.form.addField 事件,这个事件在el-form 中有监听,将加入 el-form 的 fields 字段集合中;设置初始值,以及添加校验事件;
这就解释我了我们的疑问:el.form.addField 在哪儿触发的? 他是在 el-form-item mounted后 根据prop 判断存在后触发的。
beforeDestroy 部分
javascript
beforeDestroy() {
this.dispatch('ElForm', 'el.form.removeField', [this]);
}
这个就是 在自身销毁的之前,触发 el.form.removeField 吧自从从 el-from 的 fields 中移除;
至此 我们的 el-form-item 就基本分析完毕了
相信通过这两篇文章 恭喜你你对 el-form 的设计 和 使用又有了一些新的认识 ~~~
最后的最后
下面请上我们今天的主角:有请小趴菜