开始看ElementUI的组件库源码,学习一下优秀的组件实现方式。单个组件来进行学习。
本文涉及到基础组件。
注意点如下:
- disabled通过disabled和loading两个状态来判断
- 通过type和size属性,来生成不同的class实现不同样式的渲染,其他如round、circle等属性也是如此。
- 通过$slots.default渲染默认插槽内容
- click事件触发的时候传递event事件对象
简略版实现如下:
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| <template> <button class="el-button" @click="handleClick" :disabled="disabled || loading" :autofocus="autofocus" :type="nativeType" :class="[ type? 'el-button--'+type : '', size?'el-button--'+size : '', { 'is-disabled': disabled, 'is-loading': loading, 'is-plain': plain, 'is-round': round, 'is-circle': circle } ]" > <i v-if="loading" class="el-icon-loading"></i> <i :class="icon" v-if="!loading && icon"></i> <span v-if="$slots.default"><slot></slot></span> </button>
</template> <script> export default { name: "ElButton", props: { type: { type: String, default: "" }, nativeType: { type:String, default: "button" }, size:String, icon:{ type:String, default:"" }, loading:Boolean, disabled:Boolean, plain: Boolean, autofocus: Boolean, round: Boolean, circle: Boolean }, methods: { handleClick(evt){ this.$emit("click", evt) } } }
</script> <style lang="scss" scoped> // 样式代码略,可以查看packages/theme-chalk/src/button.scss </style>
|
当然原版代码考虑了更多内容:
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| <template> <button class="el-button" @click="handleClick" :disabled="buttonDisabled || loading" :autofocus="autofocus" :type="nativeType" :class="[ type ? 'el-button--' + type : '', buttonSize ? 'el-button--' + buttonSize : '', { 'is-disabled': buttonDisabled, 'is-loading': loading, 'is-plain': plain, 'is-round': round, 'is-circle': circle } ]" > <i class="el-icon-loading" v-if="loading"></i> <i :class="icon" v-if="icon && !loading"></i> <span v-if="$slots.default"><slot></slot></span> </button> </template> <script> export default { name: 'ElButton', inject: { elForm: { default: '' }, elFormItem: { default: '' } },
props: { type: { type: String, default: 'default' }, size: String, icon: { type: String, default: '' }, nativeType: { type: String, default: 'button' }, loading: Boolean, disabled: Boolean, plain: Boolean, autofocus: Boolean, round: Boolean, circle: Boolean }, computed: { _elFormItemSize() { return (this.elFormItem || {}).elFormItemSize; }, buttonSize() { return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size; }, buttonDisabled() { return this.$options.propsData.hasOwnProperty('disabled') ? this.disabled : (this.elForm || {}).disabled; } },
methods: { handleClick(evt) { this.$emit('click', evt); } } }; </script>
|
ButtonGroup组件没太多内容,就是通过插槽渲染Button
1 2 3 4 5
| <template> <div class="el-button-group"> <slot></slot> </div> </template>
|
ButtonGroup中Button的渲染形式不同是通过样式实现的。
ButtonGroup的样式还是有可聊之处的,ButtonGroup的样式引入了utils-clearfix,引入的这个样式的写法挺巧妙的,是我之前没想到过的写法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @mixin utils-clearfix { $selector: &; @at-root { #{$selector}::before, #{$selector}::after { display: table; content: ""; } #{$selector}::after { clear: both } } }
|
ICON组件
icon组件的实现更为简单
- 通过给定name给予不同的样式名
- 根据样式名渲染不同的icon(使用font-family的方式实现)
1 2 3 4 5 6 7 8 9 10 11 12 13
| <template> <i :class="'el-icon-' + name"></i> </template>
<script> export default { name: 'ElIcon',
props: { name: String } }; </script>
|
LINK组件
Link组件的实现也很简单,通过传入不同的type来渲染不同样式的链接,通过href指定跳转路径,通过icon属性指定icon图标,通过disabled指定是否禁用。
只是多了slot插槽内容用于自定义:
需要注意的是click事件,只有在非禁用且href不存在的情况下才会被触发。
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| <template> <a :class="[ 'el-link', type ? `el-link--${type}` : '', disabled && 'is-disabled', underline && !disabled && 'is-underline' ]" :href="disabled ? null : href" v-bind="$attrs" @click="handleClick" >
<i :class="icon" v-if="icon"></i>
<span v-if="$slots.default" class="el-link--inner"> <slot></slot> </span>
<template v-if="$slots.icon"><slot v-if="$slots.icon" name="icon"></slot></template> </a> </template>
<script>
export default { name: 'ElLink',
props: { type: { type: String, default: 'default' }, underline: { type: Boolean, default: true }, disabled: Boolean, href: String, icon: String },
methods: { handleClick(event) { if (!this.disabled) { if (!this.href) { this.$emit('click', event); } } } } }; </script>
|
LAYOUT组件
layout布局依赖于row和col两个组件,分别看他们两个:
ROW
row组件占据一行,接收以下属性:
1 2 3 4 5 6 7 8 9 10 11
| tag: { type: String, default: 'div' }, gutter: Number, type: String, justify: { type: String, default: 'start' }, align: String
|
生成间隔的方式很有意思,通过间隔/2加到两侧边距上,当然间隔这个东西得在列上加才能看到效果,后边看一下在col组件如何处理的:
1 2 3 4 5 6 7 8 9 10 11 12
| computed: { style() { const ret = {};
if (this.gutter) { ret.marginLeft = `-${this.gutter / 2}px`; ret.marginRight = ret.marginLeft; }
return ret; } },
|
row组件使用h函数写的,内容比较简单很容易看懂:
1 2 3 4 5 6 7 8 9 10 11
| render(h) { return h(this.tag, { class: [ 'el-row', this.justify !== 'start' ? `is-justify-${this.justify}` : '', this.align ? `is-align-${this.align}` : '', { 'el-row--flex': this.type === 'flex' } ], style: this.style }, this.$slots.default); }
|
不难看出,默认渲染标签是div,中间的col组件通过插槽加载。
justify和align用于布局,仅在flex布局下生效,代码中没有体现,但不是flex布局下,即便加了justify-content样式也是无效的。
COL
col组件接收如下属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| props: { span: { type: Number, default: 24 }, tag: { type: String, default: 'div' }, offset: Number, pull: Number, push: Number, xs: [Number, Object], sm: [Number, Object], md: [Number, Object], lg: [Number, Object], xl: [Number, Object] },
|
row上定义的gutter终于要用到了:
1 2 3 4 5 6 7 8 9 10 11
| computed: { gutter() { let parent = this.$parent; while (parent && parent.$options.componentName !== 'ElRow') { parent = parent.$parent; } return parent ? parent.gutter : 0; } },
|
也是使用h函数渲染的:
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 38 39
| render(h) { let classList = []; let style = {}; if (this.gutter) { style.paddingLeft = this.gutter / 2 + 'px'; style.paddingRight = style.paddingLeft; } ['span', 'offset', 'pull', 'push'].forEach(prop => { if (this[prop] || this[prop] === 0) { classList.push( prop !== 'span' ? `el-col-${prop}-${this[prop]}` : `el-col-${this[prop]}` ); } }); ['xs', 'sm', 'md', 'lg', 'xl'].forEach(size => { if (typeof this[size] === 'number') { classList.push(`el-col-${size}-${this[size]}`); } else if (typeof this[size] === 'object') { let props = this[size]; Object.keys(props).forEach(prop => { classList.push( prop !== 'span' ? `el-col-${size}-${prop}-${props[prop]}` : `el-col-${size}-${props[prop]}` ); }); } });
return h(this.tag, { class: ['el-col', classList], style }, this.$slots.default); }
|
只是简单的过一下,没有去深究很详尽的细节,elemenu的基础组件就到这里了。