感受如今的业务开发,若是不是很特殊的需求,基本都能在对应的组件库内找到组件使用,这样编写代码就成了调用组件,可是却隐藏了组件内的思想,所以弱化了编程能力,因此我想写这么个分析系列来鞭策本身深刻分析组件的原理,提升代码阅读理解能力,我以为必定要记下点什么来,若是只是看不动笔感受很快就忘了,所以准备持续写这么个分析
css
官网传送门点此, 主要目录以下图html
package
目录下,
src
中是一些工具函数(某些组件都会使用这些函数)和国际化相关的代码,进入
package
目录里,则是全部组件的源码
theme-chalk
文件夹里,整个项目结构仍是很清晰
<el-row>
源码分析首先进入打开官网查看Layout
相关部分的说明,发现主要的组件就2个: el-row
,el-col
,这2个分别表明行的容器和里面列的容器,相似于bootstrap
的col
和row
,首先咱们查看el-row
的实现,进入package
里面的row
文件夹,里面是一个src
文件夹和index.js
文件vue
index.js
,这里最后一句导出
Row
供咱们
import
,而中间的
install
方法则是把这个组件当成一个Vue的插件来使用,经过
Vue.use()
来使用该组件,install方法传递一个Vue的构造器,Element的全部组件都是一个对象{...},里面有个
render
函数来建立组件的html结构,
render
方法的好处很大,使得建立html模板的代码更加简洁高效,而不是冗长的各类div标签堆叠,更相似于一种配置形式来建立html. 最后经过
export default
导出,而不是经常使用的单文件组件形式,所以必须提供install方法
import Row from './src/row';
/* istanbul ignore next */
Row.install = function(Vue) {
//全局注册该组件(经常使用的组件最好全局注册)
Vue.component(Row.name, Row);
};
export default Row;
复制代码
这里其实有2种方法使用组件,一是当作插件,而是直接import后注册组件,官网示例代码以下,也能够不注册成全局组件git
import Vue from 'vue';
import { Button, Select } from 'element-ui';
import App from './App.vue';
Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
/* 或写为 * Vue.use(Button) * Vue.use(Select) */
new Vue({
el: '#app',
render: h => h(App)
});
复制代码
下面进入src/row.js
中一探究竟,首先代码的总体结构以下,直接导出一个对象,里面是组件的各类配置项github
export default {
...
}
复制代码
整个组件的代码量很少,下面是给出了详细注释express
export default {
//组件名称,注意是驼峰命名法,这使得实际使用组件时短横线链接法<el-row>和驼峰法<ElRow>均可以使用
name: 'ElRow',
//自定义属性(该属性不是component必需属性),重要,用于后面<el-col>不断向父级查找该组件
componentName: 'ElRow',
//组件的props
props: {
//组件渲染成html的实际标签,默认是div
tag: {
type: String,
default: 'div'
},
//该组件的里面的<el-col>组件的间隔
gutter: Number,
/* 组件是不是flex布局,将 type 属性赋值为 'flex',能够启用 flex 布局,
* 并可经过 justify 属性来指定 start, center, end, space-between, space-around
* 其中的值来定义子元素的排版方式。
*/
type: String,
//flex布局的justify属性
justify: {
type: String,
default: 'start'
},
//flex布局的align属性
align: {
type: String,
default: 'top'
}
},
computed: {
//row的左右margin,用于抵消col的padding,后面详细解释,注意是计算属性,这里经过gutter计算出实际margin
style() {
const ret = {};
if (this.gutter) {
ret.marginLeft = `-${this.gutter / 2}px`;
ret.marginRight = ret.marginLeft;
}
return ret;
}
},
render(h) {
//渲染函数,后面详细解释
return h(this.tag, {
class: [
'el-row',
this.justify !== 'start' ? `is-justify-${this.justify}` : '',
this.align !== 'top' ? `is-align-${this.align}` : '',
{ 'el-row--flex': this.type === 'flex' }
],
style: this.style
}, this.$slots.default);
}
};
复制代码
下面说一下计算属性里面的sytle()
,这里面经过gutter
属性计算出了本组件的左右margin,且为负数,这里有点费解,下面上图解释,首先gutter
的做用是让row里面的col产生出间隔来,可是注意容器的最左和最右侧是没有间隔的编程
<el-row>
的宽度范围,里面是
<el-col>
组件,下一节介绍, 这个组件的宽度其实按
<el-row>
百分比来计算,并且
box-sizing
是
border-box
,注意
gutter
属性是定义在父级的
<el-row>
上,子级的col经过
$parent
能够拿到该属性,而后给
<el-col>
分配
padding-left
和
padding-right
,所以每一个col都有左右padding,上图中每一个col占宽25%,gutter的宽度就是col的padding的2倍,可是注意最左侧和最右侧是没有padding的,那么问题来了,怎么消去最左和最右的padding? 这里就是
<el-row>
负的margin起的做用,若是不设置上面的计算属性的style,那么左右2侧就会有col的padding,所以这里负的margin抵消了col的padding,且该值为
-gutter/2+'px'
注意若是初看上面的图,通常的想法是col之间用margin来间隔,实际上是不行的,而用padding来间隔就很简单,width按百分比来分配就行(box-sizing要设置为border-box)
element-ui
下面解释下最后返回的渲染函数render
,这个函数有3个参数,第一个参数是html的tag名称(最终在网页中显示的标签名),第二个参数是一个包含模板相关属性的数据对象,里面有至关多模板相关的属性,以下bootstrap
{
// 和`v-bind:class`同样的 API
// 接收一个字符串、对象或字符串和对象组成的数组
'class': {
foo: true,
bar: false
},
// 和`v-bind:style`同样的 API
// 接收一个字符串、对象或对象组成的数组
style: {
color: 'red',
fontSize: '14px'
},
// 正常的 HTML 特性
attrs: {
id: 'foo'
},
// 组件 props
props: {
myProp: 'bar'
},
// DOM 属性
domProps: {
innerHTML: 'baz'
},
// 事件监听器基于 `on`
// 因此再也不支持如 `v-on:keyup.enter` 修饰器
// 须要手动匹配 keyCode。
on: {
click: this.clickHandler
},
// 仅对于组件,用于监听原生事件,而不是组件内部使用
// `vm.$emit` 触发的事件。
nativeOn: {
click: this.nativeClickHandler
},
// 自定义指令。注意,你没法对 `binding` 中的 `oldValue`
// 赋值,由于 Vue 已经自动为你进行了同步。
directives: [
{
name: 'my-custom-directive',
value: '2',
expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
// 做用域插槽格式
// { name: props => VNode | Array<VNode> }
scopedSlots: {
default: props => createElement('span', props.text)
},
// 若是组件是其余组件的子组件,需为插槽指定名称
slot: 'name-of-slot',
// 其余特殊顶层属性
key: 'myKey',
ref: 'myRef'
}
复制代码
尤为注意第三个参数,它表明子节点,是一个String
或者Array
,当是String
时表明文本节点的内容,此时这就是个文本节点,若是是Array
,里面就是子节点,数组中每一个值都是一个render的参数函数数组
[
//文本节点
'先写一些文字',
createElement('h1', '一则头条'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
复制代码
再看上面render函数的第三个参数是this.$slots.default
,这里的意思就是获取该组件下面不是具名插槽的内容,default 属性包括了全部没有被包含在具名插槽中的节点,对于以下代码,该render函数就会把<el-row>
以及<h1>test<h1>
做为其子节点一块儿渲染出来
<el-row>
<h1>test<h1>
<slot name='t'>t1</slot>
</el-row>
复制代码
最后解释下样式相关代码,row.scss
的路径是packages/theme-chalk/src/row.scss
,代码是scss类型,render里的class以下
class:[
'el-row',
this.justify !== 'start' ? `is-justify-${this.justify}` : '',
this.align !== 'top' ? `is-align-${this.align}` : '',
{ 'el-row--flex': this.type === 'flex' }
],
复制代码
这里的el-row
类其实没有定义,能够本身在写代码时补充,官网就是这么用的,后面几个都是控制flex布局的,因而可知<el-row>
默认占满父容器宽度且高度auto自适应
<el-col>
源码分析span
,offset
,pull
,push
等属性<el-col :span="6" :offset="6"><div class="grid-content bg-purple"></div></el-col>
复制代码
进入package/col
查看,col的代码稍长,主要多出来的逻辑是控制自适应(@media screen)
export default {
//组件名称
name: 'ElCol',
props: {
//组件占父容器的列数,总共24列,若是设置为0则渲染出来display为none
span: {
type: Number,
default: 24
},
//最终渲染出的标签名,默认div
tag: {
type: String,
default: 'div'
},
//经过制定 col 组件的 offset 属性能够指定分栏向右偏移的栏数
offset: Number,
//栅格向右移动格数
pull: Number,
//栅格向左移动格数
push: Number,
//响应式相关
xs: [Number, Object],
sm: [Number, Object],
md: [Number, Object],
lg: [Number, Object],
xl: [Number, Object]
},
computed: {
//获取el-row的gutter值
gutter() {
let parent = this.$parent;
//不断经过获取父元素直到找到el-row元素位置,注意这里的技巧,componentName实际
//是el-row组件设置的一个自定义属性,用来判断是不是el-row组件
while (parent && parent.$options.componentName !== 'ElRow') {
parent = parent.$parent;
}
return parent ? parent.gutter : 0;
}
},
render(h) {
let classList = [];
let style = {};
//经过gutter计算本身的左右2个padding,达到分隔col的目的
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);
}
};
复制代码
下面解释下['span', 'offset', 'pull', 'push']
这几个的做用,span很好理解,占父容器的列数,对应scss代码以下
[class*="el-col-"] {
float: left;
box-sizing: border-box;
}
.el-col-0 {
display: none;
}
@for $i from 0 through 24 {
.el-col-#{$i} {
width: (1 / 24 * $i * 100) * 1%;
}
.el-col-offset-#{$i} {
margin-left: (1 / 24 * $i * 100) * 1%;
}
.el-col-pull-#{$i} {
position: relative;
right: (1 / 24 * $i * 100) * 1%;
}
.el-col-push-#{$i} {
position: relative;
left: (1 / 24 * $i * 100) * 1%;
}
}
复制代码
注意上面的[attribute*=value] 选择器,它选择了全部类名以el-col-
开头的类,加上float和border-box,水平布局float确定不可少,再看for循环,这里scss的威力就发挥了,若是只用css,那代码量要乘以24,el-col-数字
类型的类的宽度就是百分比,下面的offset
其实是margin-left
,这可能会致使一行排列不下全部的col,会致使换行出现,而el-col-pull
则不一样,仅仅只是相对原来的位置移动,不会形成挤下去换行的状况,而会形成不一样col互相覆盖
注意上面的js部分大量使用模板字符串而不是字符串拼接,达到简化代码的目的,这个值得学习