elementui组件_ElementUI技术揭秘(3)—Layout布局组件的
设计与实现
前⾔
当我们拿到⼀个 PC 端页⾯的设计稿的时候,往往会发现页⾯的布局并不是随意的,⽽是遵循的⼀定的规律:⾏与⾏之间会以某种⽅式对齐。对于这样的设计稿,我们可以使⽤栅格布局来实现。
早在 Bootstrap ⼀统江湖的时代,栅格布局的概念就已深⼊⼈⼼,整个布局就是⼀个⼆维结构,包括列和⾏, Bootstrap 会把屏幕分成12 列,还提供了⼀些⾮常⽅便的 CSS 名让我们来指定每列占的宽度百分⽐,并且还通过媒体查询做了不同屏幕尺⼨的适应。
element-ui 也实现了类似 Bootstrap 的栅格布局系统,那么基于 Vue 技术栈,它是如何实现的呢?
需求分析
和 Bootstrap 12 分栏不同的是,element-ui ⽬标是提供的是更细粒度的 24 分栏,迅速简便地创建布局,写法⼤致如下:
<el-row>
<el-col>aaael-col>
<el-col>bbbel-col>
el-row>
<el-row>
...
el-row>
这就是⼆维布局的雏形,我们会把每个列的内容写在 之间,除此之外,我们还需要⽀持控制每个 所占的宽度⾃由组合布局;⽀持分栏之间存在间隔;⽀持偏移指定的栏数;⽀持分栏不同的对齐⽅式等。
了解了 element-ui Layout 布局组件的需求后,我们来分析它的设计和实现。
设计和实现
组件的渲染
回顾前⾯的例⼦,从写法上看,我们需要设计 2 个组件,el-row 和 el-col 组件,分别代表⾏和列;从 Vue 的语法上看,这俩组件都要⽀持插槽(因为在⾃定义组件标签内部的内容都分发到组件的 slot 中了);从 HTML 的渲染结果上看,我们希望模板会渲染成:
<div class="el-row">
<div class="el-col">aaadiv>
<div class="el-col">bbbdiv>
div>
<div class="el-row">
...
div>
想达到上述需求,组件的模板可以⾮常简单。
el-row 组件模板代码如下:
<div class="el-row">
<slot>slot>
div>
el-col 组件代码如下:
<div class="el-col">
<slot>slot>
div>
这个时候,新需求来了,我希望 el-row 和 el-col 组件不仅能渲染成 div,还可以渲染成任意我想指定的标签。
那么除了我们要⽀持⼀个 tag 的 prop 之外,仅⽤模板是难以实现了。
我们知道 Vue 的模板最终会编译成 render 函数,Vue 的组件也⽀持直接⼿写 render 函数,那这个需求⽤ render 函数实现就⾮常简单了。
el-row 组件:
render(h) {
return h(this.tag, {
class: [
'el-row',
]
}, this.$slots.default);
}
el-col 组件:
render(h) {
return h(this.tag, {
class: [
'el-col',
]
}, this.$slots.default);
}
其中,tag 是定义在 props 中的,h 是 Vue 内部实现的 $createElement 函数,如果对 render 函数语法还不太懂的同学,建议去看 Vue 的官⽹⽂档 render 函数部分。
了解了组件是如何渲染之后,我们来给 Layout 组件扩展⼀些 feature 。
分栏布局
Layout 布局的主要⽬标是⽀持 24 分栏,即⼀⾏能被切成 24 份,那么对于每⼀个 el-col ,我们想要知道它的占⽐,只需要指定它在 24份中分配的份数即可。
于是我们给刚才的⽰例加上⼀些配置:
flex布局对齐方式<el-row>
<el-col :span="8">aaael-col>
<el-col :span="16">bbbel-col>
el-row>
<el-row>
...
el-row>
来看第⼀⾏,第⼀列 aaa 占 8 份,第⼆列 bbb 占 16 份。总共宽度是 24 份,经过简单的数学公式计算,aaa 占总宽度的 1/3,⽽ bbb 占总宽度的 2/3,进⽽推导出每⼀列指定 span 份就是占总宽度的 span/24。
默认情况下 div 的宽度是 100% 独占⼀⾏的,为了让多个 el-col 在⼀⾏显⽰,我们只需要让每个 el-col 的宽占⼀定的百分⽐,即实现了分栏效果。设置不同的宽度百分⽐只需要设置不同的 CSS 即可实现,⽐如当某列占 12 份的时候,那么它对应的 CSS 如下:
.el-col-12 {
width: 50%
}
为了满⾜ 24 种情况,element-ui 使⽤了 sass 的控制指令,配合基本的计算公式:
.el-col-0 {
display: none;
}
@for $i from 0 through 24 {
.el-col-#{$i} {
width: (1 / 24 * $i * 100) * 1%;
}
}
所以当我们给 el-col 组件传⼊了 span 属性的时候,只需要给对应的节点渲染⽣成对应的 CSS 即可,于是我们可以扩展 render 函数:
render(h) {
let classList = [];
classList.push(`el-col-${this.span}`);
return h(this.tag, {
class: [
'el-col',
classList
]
}, this.$slots.default);
}
这样只要指定 span 属性的列就会添加 el-col-${span} 的样式,实现了分栏布局的需求。
分栏间隔
对于栅格布局来说,列与列之间有⼀定间隔空隙是常见的需求,这个需求的作⽤域是⾏,所以我们应该给 el-row 组件添加⼀个 gutter 的配置,如下:
<el-row :gutter="20">
<el-col :span="8">aaael-col>
<el-col :span="16">bbbel-col>
el-row>
<el-row>
...
el-row>
有了配置,接下来如何实现间隔呢?实际上⾮常简单,想象⼀下,2 个列之间有 20 像素的间隔,如果我们每列各往⼀边收缩 10 像素,是不是看上去就有 20 像素了呢。
先看⼀下 el-col 组件的实现:
computed: {
gutter() {
let parent = this.$parent;
while (parent && parent.$optionsponentName !== 'ElRow') {
parent = parent.$parent;
}
return parent ? parent.gutter : 0;
}
},
render(h) {
let classList = [];
classList.push(`el-col-${this.span}`);
let style = {};
if (this.gutter) {
style.paddingLeft = this.gutter / 2 + 'px';
style.paddingRight = style.paddingLeft;
}
return h(this.tag, {
class: [
'el-col',
classList
]
}, this.$slots.default);
}
这⾥使⽤了计算属性去计算 gutter,其实是⽐较有趣的,它通过 $parent 往外层查 el-row,获取到组件的实例,然后获取它的 gutter 属性,这样就建⽴了依赖关系,⼀旦 el-row 组件的 gutter 发⽣变化,这个计算属性再次被访问的时候就会重新计算,获取到新的 gutter。
其实,想在⼦组件去获取祖先节点的组件实例,我更推荐使⽤ provide/inject 的⽅式去把祖先节点的实例注⼊到⼦组件中,这样⼦组件可以⾮常⽅便地拿到祖先节点的实例,⽐如我们在 el-row 组件编写 provide:
provide() {
return {
row: this
};
}
然后在 el-col 组件注⼊依赖:
inject: ['row']
这样在 el-col 组件中我们就可以通过 w 访问到 el-row 组件实例了。
使⽤ provide/inject 的好处在于不论组件层次有多深,⼦孙组件可以⽅便地访问祖先组件注⼊的依赖。
当你在编写组件库的时候,遇到嵌套组件并且⼦组件需要访问⽗组件实例的时候,避免直接使⽤ this.$parent,尽量使⽤ provide/inject,因为⼀旦你的组件嵌套关系发⽣变
化,this.$parent 可能就不符合预期了,⽽ provide/inject 却不受影响(只要祖先和⼦孙的关系不变)。
在 render 函数中,我们会根据 gutter 计算,给当前列添加了 paddingLeft 和 paddingRight 的样式,值是 gutter 的⼀半,这样就实现了间隔gutter 的效果。
那么这⾥能否⽤ margin 呢,答案是不能,因为设置 margin 会占⽤外部的空间,导致每列的占⽤空间变⼤,会出现折⾏的情况。
render 过程也是有优化的空间,因为 style 是根据 gutter 计算的,那么我们可以把 style 定义成计算属性,这样只要 gutter 不变,那么 style 就可以直接拿计算属性的缓存,⽽不⽤重新计算,对于 classList 部分,我们同样可以使⽤计算属性。组件 render 过程的⼀个原则就是能⽤计算属性就⽤计算属性。
再来看⼀下 el-row 组件的实现:
computed: {
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',
],
style: this.style
}, this.$slots.default);
}
由于我们是通过给每列添加左右 padding 的⽅式来实现列之间的间隔,那么对于第⼀列和最后⼀列,左边和右边也会多出来 gutter/2 ⼤⼩的间隔,显然是不符合预期的,所以我们可以通过设置左右负 margin 的⽅式填补左右的空⽩,这样就完美实现了分栏间隔的效果。
偏移指定的栏数
如图所⽰,我们也可以指定某列的偏移,由于作⽤域是列,我们应该给 el-col 组件添加⼀个 offset 的配置,如下:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论