动机
感觉现在的业务开发,如果不是很特殊的需求,基本都能在对应的组件库内找到组件使用,这样编写代码就成了调用组件,但是却隐藏了组件内的思想,因此弱化了编程能力,所以我想写这么个分析系列来鞭策自己深入分析组件的原理,提高代码阅读理解能力,我觉得一定要记下点什么来,如果只是看不动笔感觉很快就忘了,因此准备持续写这么个分析
Element源码结构
官网传送门点此, 主要目录如下图
其中组件的源码放在
package
目录下,
src
中是一些工具函数(某些组件都会使用这些函数)和国际化相关的代码,进入
package
目录里,则是所有组件的源码
注意这些文件夹里只包含js或者vue,而所有组件的样式文件在最下面的
theme-chalk
文件夹里,整个项目结构还是很清晰
Layout(布局)源码分析
<el-row>
源码分析
首先进入打开官网查看Layout
相关部分的说明,发现主要的组件就2个: el-row
,el-col
,这2个分别代表行的容器和里面列的容器,类似于bootstrap
的col
和row
,首先我们查看el-row
的实现,进入package
里面的row
文件夹,里面是一个src
文件夹和index.js
文件
打开
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后注册组件,官网示例代码如下,也可以不注册成全局组件
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
中一探究竟,首先代码的整体结构如下,直接导出一个对象,里面是组件的各种配置项
export default {
...
}
整个组件的代码量不多,下面是给出了详细注释
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)
下面解释下最后返回的渲染函数render
,这个函数有3个参数,第一个参数是html的tag名称(最终在网页中显示的标签名),第二个参数是一个包含模板相关属性的数据对象,里面有相当多模板相关的属性,如下
{
// 和`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>
源码分析
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部分大量使用模板字符串而不是字符串拼接,达到简化代码的目的,这个值得学习
今天的文章Element源码分析系列1一Layout(布局)分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/21137.html