最近一直在用vue来开发页面,使用的ui框架是饿了么的前端框架Element-ui,个人还在一直学习的阶段。用了那么久的Element,但是它的内部到底是怎么实现的?I know nothing…Jhon S…咳咳,我也不知道它是怎么实现的。
现如今心血来潮,想分析一下Element的源码,也能进一步了解vue的使用。于是挑选了比较常用的表单插件 el-form 来进行分析。
组件的使用 先来看一段Element官网的demo:
1 2 3 4 5 6 7 8 9 <el-form :model="numberValidateForm" ref="numberValidateForm" label-width="100px" class="demo-ruleForm"> <el-form-item label="年龄" prop="age"> <el-input type="age" v-model.number="numberValidateForm.age" auto-complete="off"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="submitForm('numberValidateForm')">提交</el-button> <el-button @click="resetForm('numberValidateForm')">重置</el-button> </el-form-item> </el-form>
我们使用el-form的时候,表单的每一项我们都要使用到el-form-item,然后el-form-item里面再用到各个类型的输入框。
如果我们是第一次使用vue,以前用的都是传统的html、jsp的页面,看到这里可能有点疑惑:el-form?el-form-item?这些是什么标签?并不是我们常见的html标签啊……
其实这都是Element的组件,Element已经把这些组件声明为全局组件 ,vue使用组件的时候可以直接使用。
我们可以根据组件的name属性,来使用同名的标签,表明我们使用的是这个特定的组件。使用组件的时候有一个有趣的地方,组件的name属性我们一般用驼峰命名,但是我们使用标签的时候,要用连接线 的方式才行。比如el-form标签,其name属性为:
使用slot来嵌套组件
为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个过程被称为内容分发 (即 Angular 用户熟知的“transclusion”)。Vue.js 实现了一个内容分发 API,参照了当前 Web Components 规范草案 ,使用特殊的 <slot>
元素作为原始内容的插槽。
实际上,el-form里面会嵌套着el-form-item,用到的正是vue的slot 功能。slot是用来分发内容的,就是可以把东西放入到el-form组件里面。
单个插槽 1 2 3 4 5 6 7 8 <template > <form class ="el-form" :class ="[ labelPosition ? 'el-form--label-' + labelPosition : '', { 'el-form--inline': inline } ]" > <slot > </slot > </form > </template >
我们来看看el-form的源码中的html代码部分,比较简单,form标签里面只有一个slot插槽。我们把el-form-item 放在el-form 标签里面的时候,el-form-item 就会被插入到slot的位置。
这就是使用slot的方法:你把其他的html元素放入到该组件的标签里面,这些html元素就会被插入到组件的slot标签的位置。
所以我们平时能像html一样使用这些组件的标签,就是使用了slot。
具名插槽 除了单个插槽,还有具名插槽。顾名思义,就是有名字的插槽,插槽有一个name属性。比如el-form-item 里面用的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <div class ="el-form-item" :class ="{ 'is-error': validateState === 'error', 'is-validating': validateState === 'validating', 'is-required': isRequired || required }" > <label :for ="prop" class ="el-form-item__label" v-bind:style ="labelStyle" v-if ="label || $slots.label" > <slot name ="label" > {{label + form.labelSuffix}} </slot > </label > <div class ="el-form-item__content" v-bind:style ="contentStyle" > <slot > </slot > <transition name ="el-zoom-in-top" > <div class ="el-form-item__error" v-if ="validateState === 'error' && showMessage && form.showMessage" > {{validateMessage}}</div > </transition > </div > </div >
可以看到,label 标签下使用了具名slot.在父组件调用的时候,可以在label下的某个标签附上属性slot=”label”,则会被插入到这个具名slot的位置。一般我们不需要自定义标签,所以不会用到这个具名slot.这时会使用这个slot的默认内容NaN。(slot里面的内容就是备用内容,如果用户没有放别的东西进来,就默认显示备用内容)
例如:
1 <h1 slot ="label" > 这里可能是一个页面标题</h1 >
transition过渡 顺带一提,这里还用到了transition 标签,transition 标签是VUE提供的过渡效果的标签。VUE会根据name属性去查找CSS。当插入或删除包含在 transition 组件中的元素时,Vue 将会做以下处理:
自动嗅探目标元素是否使用了 CSS 过渡或动画,如果使用,会在合适的时机添加/移除CSS 过渡 class。
如果过渡组件设置了 JavaScript 钩子函数,这些钩子函数将在合适的时机调用。
如果没有检测到 CSS 过渡/动画,并且也没有设置 JavaScript钩子函数,插入和/或删除 DOM 的操作会在下一帧中立即执行。(注意:这里的帧是指浏览器逐帧动画机制,和 Vue 的 nextTick 概念不同)
el-form-item和el-form既然是2个不同的插件,我们把表单对象绑定到el-form的model属性的时候,el-form-item是怎么获取到的呢?
我在el-form-item源码里发现了计算属性form:
1 2 3 4 5 6 7 8 9 10 11 12 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; }
el-form-item通过$parent 获取el-form实例,这里并不只是获取el-form那么简单,如果item是嵌套在另一个item里面的,还要继续往上获取form,直到获取到form。
接下来,根据el-form-item的prop属性(开发者用于绑定表单对象中的字段的属性)来获取model中的值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 fieldValue: { cache: false , get() { var model = this .form.model; if (!model || !this .prop) { return ; } var path = this .prop; if (path.indexOf(':' ) !== -1 ) { path = path.replace(/:/ , '.' ); } return getPropByPath(model, path).v; } }, function getPropByPath (obj, path ) { let tempObj = obj; path = path.replace(/\[(\w+)\]/g , '.$1' ); path = path.replace(/^\./ , '' ); let keyArr = path.split('.' ); let i = 0 ; for (let len = keyArr.length; i < len - 1 ; ++i) { let key = keyArr[i]; if (key in tempObj) { tempObj = tempObj[key]; } else { throw new Error ('please transfer a valid prop path to form item!' ); } } return { o: tempObj, k: keyArr[i], v: tempObj[keyArr[i]] }; }
小伙伴们看一下代码就明白了,根据prop的key,获取到model中对应的值。
属性的默认值 1 2 3 4 showMessage: { type: Boolean , default : true }
使用default来赋予默认值,type指定属性的种类,可以有多个种类,用[Boolean,String]的格式。
图标的设置 平时我们可以看到一些按钮上可能有一些图标,比如这篇文章的标题下面,时间、分类的前面都有个小图标,这可不是插入了一张小图片,而是用了矢量图标,这实际上是通过设置class属性来实现的。
我们来看看常用的el-input,也就是我们的输入框组件,它有一个icon属性,我们可以根据 Element图标 来设置icon属性。官网的原文是这么说的:
直接通过设置类名为 el-icon-iconName
来使用即可。
它都这么说了,我就乖乖地把icon=”el-icon-el-icon-search”,想设置一个搜索图标。惊喜地发现了&¥#@&……¥¥&#根本不管用啊!!
下面我们看看它的代码实现:
1 2 3 4 5 6 7 8 9 10 <slot name ="icon" > <i class ="el-input__icon" :class ="[ 'el-icon-' + icon, onIconClick ? 'is-clickable' : '' ]" v-if ="icon" @click ="handleIconClick" > </i > </slot >
可以发现,只要根据官网的图标的名字,把 el-icon- 后面的名字作为icon属性就行了,根本不是官网说的那样。你自己做了处理还不说清楚……
$slots获取slot内容 1 2 3 4 <div class ="el-input-group__prepend" v-if ="$slots.prepend" > <slot name ="prepend" > </slot > </div >
这里用到了$slots ,可以通过$slots.具名slot的name来获取这个slot的内容,来判断是否有前置元素的存在。
1 2 3 4 5 6 7 8 9 10 11 <input v-if ="type !== 'textarea'" class ="el-input__inner" v-bind ="$props" :autocomplete ="autoComplete" :value ="currentValue" ref ="input" @input ="handleInput" @focus ="handleFocus" @blur ="handleBlur" >
上面这段代码是el-input里面的input标签,想想也是,el-input只是在原生的input标签上封装了一层而已。
这里有3个事件input,focus,blur,这里Element没有做过多的处理,都是用$emit 来触发当前实例的事件(我理解为el-input组件的上层所定义的事件,@click之类的,交给用户自己来决定怎么处理),比如$emit(‘input’, value)可以把el-input组件的值传回我们的实例v-model绑定的值,来实现组件之间的值传递。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 handleFocus(event) { this .$emit('focus' , event); }, handleInput(event) { const value = event.target.value; this .$emit('input' , value); this .setCurrentValue(value); this .$emit('change' , value); }, handleIconClick(event) { if (this .onIconClick) { this .onIconClick(event); } this .$emit('click' , event); },
表单验证 我们可以看官网的例子:点我点我点我看例子
Form 组件提供了表单验证的功能,只需要通过 rule 属性传入约定的验证规则,并 Form-Item 的 prop属性设置为需校验的字段名即可。
但是本质上他们是怎么实现表单验证的呢?
在el-form里的mounted钩子,获取rules.可以看出同时获取了form的rules和form-item里面的rules。
1 2 3 4 5 let rules = this .getRules();if (rules.length) { this .$on('el.form.blur' , this .onFieldBlur); this .$on('el.form.change' , this .onFieldChange); }
1 2 3 4 5 6 7 8 getRules() { var formRules = this .form.rules; var selfRules = this .rules; formRules = formRules ? formRules[this .prop] : []; return [].concat(selfRules || formRules || []); },
然后开始监听blur和change事件。这2个事件在el-input里我们也看到了,在发生input的时候,el-input就会触发这2个事件。
1 2 3 4 5 6 7 8 9 10 onFieldBlur() { this .validate('blur' ); }, onFieldChange() { if (this .validateDisabled) { this .validateDisabled = false ; return ; } this .validate('change' ); }
可以看到,实际上进行校验的实际上是validate这个函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import AsyncValidator from 'async-validator' ;validate(trigger, callback = noop) { var rules = this .getFilteredRule(trigger); if (!rules || rules.length === 0 ) { callback(); return true ; } this .validateState = 'validating' ; var descriptor = {}; descriptor[this .prop] = rules; var validator = new AsyncValidator(descriptor); var model = {}; model[this .prop] = this .fieldValue; validator.validate(model, { firstFields : true }, (errors, fields) => { this .validateState = !errors ? 'success' : 'error' ; this .validateMessage = errors ? errors[0 ].message : '' ; callback(this .validateMessage); }); },
可以看出Element也是用了async-validator库来做的验证,所以配置的rules的时候根据async-validator的规则来配置即可。