新闻资讯
Element-UI 技术揭秘(2)—Layout 结构组件的设计与实现
发布时间:2021-05-11 00:11
  |  
阅读量:
字号:
A+ A- A
本文摘要:前言当我们拿到一个 PC 端页面的设计稿的时候,往往会发现页面的结构并不是随意的,而是遵循的一定的纪律:行与行之间会以某种方式对齐。对于这样的设计稿,我们可以使用栅格结构来实现。 早在 Bootstrap 一统江湖的时代,栅格结构的观点就已深入人心,整个结构就是一个二维结构,包罗列和行, Bootstrap 会把屏幕分成 12 列,还提供了一些很是利便的 CSS 名让我们来指定每列占的宽度百分比,而且还通过媒体查询做了差别屏幕尺寸的适应。

AG真人国际厅

前言当我们拿到一个 PC 端页面的设计稿的时候,往往会发现页面的结构并不是随意的,而是遵循的一定的纪律:行与行之间会以某种方式对齐。对于这样的设计稿,我们可以使用栅格结构来实现。

早在 Bootstrap 一统江湖的时代,栅格结构的观点就已深入人心,整个结构就是一个二维结构,包罗列和行, Bootstrap 会把屏幕分成 12 列,还提供了一些很是利便的 CSS 名让我们来指定每列占的宽度百分比,而且还通过媒体查询做了差别屏幕尺寸的适应。element-ui 也实现了类似 Bootstrap 的栅格结构系统,那么基于 Vue 技术栈,它是如何实现的呢?需求分析和 Bootstrap 12 分栏差别的是,element-ui 目的是提供的是更细粒度的 24 分栏,迅速轻便地建立结构,写法大致如下:<el-row> <el-col>aaa</el-col> <el-col>bbb</el-col></el-row><el-row> ...</el-row>复制代码这就是二维结构的雏形,我们会把每个列的内容写在 <el-col></el-col> 之间,除此之外,我们还需要支持控制每个 <el-col> 所占的宽度自由组合结构;支持分栏之间存在距离;支持偏移指定的栏数;支持分栏差别的对齐方式等。相识了 element-ui Layout 结构组件的需求后,我们来分析它的设计和实现。

设计和实现组件的渲染回首前面的例子,从写法上看,我们需要设计 2 个组件,el-row 和 el-col 组件,划分代表行和列;从 Vue 的语法上看,这俩组件都要支持插槽(因为在自界说组件标签内部的内容都分发到组件的 slot 中了);从 HTML 的渲染效果上看,我们希望模板会渲染成:<div class="el-row"> <div class="el-col">aaa</div> <div class="el-col">bbb</div></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 份中分配的份数即可。

于是我们给适才的示例加上一些设置:<el-row> <el-col :span="8">aaa</el-col> <el-col :span="16">bbb</el-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">aaa</el-col> <el-col :span="16">bbb</el-col></el-row><el-row> ...</el-row>复制代码有了设置,接下来如何实现距离呢?实际上很是简朴,想象一下,2 个列之间有 20 像素的距离,如果我们每列各往一边收缩 10 像素,是不是看上去就有 20 像素了呢。先看一下 el-col 组件的实现:computed: { gutter() { let parent = this.$parent; while (parent && parent.$options.componentName !== '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 组件中我们就可以通过 this.row 会见到 el-row 组件实例了。使用 provide/inject 的利益在于岂论组件条理有多深,子孙组件可以利便地会见祖先组件注入的依赖。

AG真人国际厅

当你在编写组件库的时候,遇到嵌套组件而且子组件需要会见父组件实例的时候,制止直接使用 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 的设置,如下:<el-row :gutter="20"> <el-col :offset="8" :span="8">aaa</el-col> <el-col :span="8">bbb</el-col></el-row><el-row> ...</el-row>复制代码直观上我们应该用 margin 来实现偏移,而且 margin 也是支持百分比的,因此实现这个需求就变得简朴了。

我们继续扩展 el-col 组件:render(h) { let classList = []; classList.push(`el-col-${this.span}`); classList.push(`el-col-offset-${this.offset}`); 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);}复制代码其中 offset 是界说在 props 中的,我们凭据传入的 offset 生成对应的 CSS 添加到 DOM 中。element-ui 同样使用了 sass 的控制指令,配合基本的盘算公式来实现这些 CSS 的界说:@for $i from 0 through 24 { .el-col-offset-#{$i} { margin-left: (1 / 24 * $i * 100) * 1%; }}复制代码对于差别偏移的分栏数,会有对应的 margin 百分比,就很好地实现分栏偏移需求。对齐方式当一行分栏的总占比和没有到达 24 的时候,我们是可以使用 flex 结构来对分栏做灵活的对齐。

对于差别的对齐方式 flex 结构提供了 justify-content 属性,所以对于这个需求,我们可以对 flex 结构做一层封装即可实现。由于对齐方式的作用域是行,所以我们应该给 el-row 组件添加 type 和 justify 的设置,如下:<el-row type="flex" justify="center"> <el-col :span="8">aaa</el-col> <el-col :span="8">bbb</el-col></el-row><el-row> ...</el-row>复制代码由于我们是对 flex 结构的封装,我们只需要凭据传入的这些 props 去生成对应的 CSS,在 CSS 中界说 flex 的结构属性即可。

我们继续扩展 el-row 组件:render(h) { return h(this.tag, { class: [ 'el-row', this.justify !== 'start' ? `is-justify-${this.justify}` : '', { 'el-row--flex': this.type === 'flex' } ], style: this.style }, this.$slots.default);}复制代码其中 type 和 justify 是界说在 props 中的,我们凭据它们传入的值生成对应的 CSS 添加到 DOM 中,接着我们需要界说对应的 CSS 样式:@include b(row) { position: relative; box-sizing: border-box; @include utils-clearfix; @include m(flex) { display: flex; &:before, &:after { display: none; } @include when(justify-center) { justify-content: center; } @include when(justify-end) { justify-content: flex-end; } @include when(justify-space-between) { justify-content: space-between; } @include when(justify-space-around) { justify-content: space-around; } }}复制代码element-ui 在编写 sass 的时候主要遵循的是 BEM 的命名规则,而且编写了许多自界说 @mixin 来配合样式名的界说。这里我们来花点时间来学习一下它们,element-ui 的自界说 @mixin 界说在 pacakages/theme-chalk/src/mixins/ 目录中,我并不会详细解释这内里的关键字,如果你对 sass 还不熟悉,我建议在学习这部门内容的时候配合 sass 的官网文档看。mixins/config.scss 中界说了一些全局变量:$namespace: 'el';$element-separator: '__';$modifier-separator: '--';$state-prefix: 'is-';复制代码mixins/mixins.scss 中界说了 BEM 的自界说 @mixin,先来看一下界说组件样式的 @mixin b:@mixin b($block) { $B: $namespace+'-'+$block !global; .#{$B} { @content; }}复制代码这个 @mixin 很好明白,$B 是内部界说的变量,它的值通过 $namespace+'-'+$block 盘算获得,注意这里有一个 !global 关键字,它表现把这个局部变量酿成全局的,意味着你也可以在其它 @mixin 中引用它。

AG真人国际厅

通过 @include 我们就可以去引用这个 @mixin,联合我们的 case 来看:@include b(row) { // xxx content}复制代码会编译成:.el-row { // xxx content}复制代码再来看表现修饰符的 @mixin m:@mixin m($modifier) { $selector: &; $currentSelector: ""; @each $unit in $modifier { $currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ","}; } @at-root { #{$currentSelector} { @content; } }}复制代码这里是允许传入的 $modifier 有多个,所以内部用了 @each,& 表现父选择器,$selector 和 $currentSelector 是内部界说的 2 个局部变量,联合我们的 case 来看:@mixin b(row) { @include m(flex) { // xxx content }} 复制代码会编译成:.el-row--flex { // xxx content}复制代码有同学可能会疑问,岂非不是:.el-row { .el-row--flex { // xxx content }}复制代码其实并不是,因为我们在该 @mixin 的内部使用了 @at-root 指令,它会把样式规则界说在根目录下,而不是嵌套在其父选择器下。最厥后看一下表现同级样式的 @mixin when:@mixin when($state) { @at-root { &.#{$state-prefix + $state} { @content; } }}复制代码这个 @mixin 也很好明白,联合我们的 case 来看:@mixin b(row) { @include m(flex) { @include when(justify-center) { justify-content: center; } }}复制代码会编译成:.el-row--flex.is-justify-center { justify-content: center;}复制代码关于 BEM 的 @mixin,常用的另有 @mixin e,用于界说组件内部一些子元素样式的,感兴趣的同学可以自行去看。再回到我们的 el-row 组件的样式,我们界说了几种flex 结构的对齐方式,然后通过传入差别的 justify 来生成对应的样式,这样我们就很好地实现了灵活对齐分栏的需求。

响应式结构element-ui 参照了 Bootstrap 的响应式设计,预设了五个响应尺寸:xs、sm、md、lg 和 xl。允许我们在差别的屏幕尺寸下,设置差别的分栏设置,由于作用域是列,所以我们应该给 el-col 组件添加 xs xs、sm、md、lg 和 xl 的设置,如下:<el-row type="flex" justify="center"> <el-col :xs="8" :sm="6" :md="4" :lg="3" :xl="1">aaa</el-col> <el-col :xs="4" :sm="6" :md="8" :lg="9" :xl="11">bbb</el-col> <el-col :xs="4" :sm="6" :md="8" :lg="9" :xl="11">ccc</el-col> <el-col :xs="8" :sm="6" :md="4" :lg="3" :xl="1">ddd</el-col></el-row><el-row> ...</el-row>复制代码同理,我们仍然是通过这些传入的 props 去生成对应的 CSS,在 CSS 中使用媒体查询去实现响应式。

我们继续扩展 el-col 组件:render(h) { let classList = []; classList.push(`el-col-${this.span}`); classList.push(`el-col-offset-${this.offset}`); ['xs', 'sm', 'md', 'lg', 'xl'].forEach(size => { classList.push(`el-col-${size}-${this[size]}`); }); 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);}复制代码其中,xs、sm、md、lg 和 xl 是界说在 props 中的,实际上 element-ui 源码还允许传入一个工具,可以设置 span 和 offset,但这部门代码我就不先容了,无非就是对工具的剖析,添加对应的样式。我们来看一下对应的 CSS 样式,以 xs 为例:@include res(xs) { .el-col-xs-0 { display: none; } @for $i from 0 through 24 { .el-col-xs-#{$i} { width: (1 / 24 * $i * 100) * 1%; } .el-col-xs-offset-#{$i} { margin-left: (1 / 24 * $i * 100) * 1%; } }}复制代码这里又界说了表现响应式的 @mixin res,我们来看一下它的实现:@mixin res($key, $map: $--breakpoints) { // 循环断点Map,如果存在则返回 @if map-has-key($map, $key) { @media only screen and #{inspect(map-get($map, $key))} { @content; } } @else { @warn "Undefeined points: `#{$map}`"; }}复制代码这个 @mixns 主要是检察 $map 中是否有 $key,如果有的话则界说一条媒体查询规则,如果没有则抛出警告。$map 参数的默认值是 $--breakpoints,界说在 pacakges/theme-chalk/src/common/var.scss 中:$--sm: 768px !default;$--md: 992px !default;$--lg: 1200px !default;$--xl: 1920px !default;$--breakpoints: ( 'xs' : (max-width: $--sm - 1), 'sm' : (min-width: $--sm), 'md' : (min-width: $--md), 'lg' : (min-width: $--lg), 'xl' : (min-width: $--xl));复制代码联合我们的 case 来看:@include res(xs) { .el-col-xs-0 { display: none; } @for $i from 0 through 24 { .el-col-xs-#{$i} { width: (1 / 24 * $i * 100) * 1%; } .el-col-xs-offset-#{$i} { margin-left: (1 / 24 * $i * 100) * 1%; } }}复制代码会编译成:@media only screen and (max-width: 767px) { .el-col-xs-0 { display: none; } .el-col-xs-1 { width: 4.16667% } .el-col-xs-offset-1 { margin-left: 4.16667% } // 后面循环的效果太长,就不贴了}复制代码其它尺寸内部的样式界说规则也是类似,这样我们就通过媒体查询界说了各个屏幕尺寸下的样式规则了。

通过传入 xs、sm 这些属性的值差别,从而生成差别样式,这样在差别的屏幕尺寸下,可以做到分栏的占宽差别,很好地满足了响应式需求。基于断点的隐藏类Element 分外提供了一系列类名,用于在某些条件下隐藏元素,这些类名可以添加在任何 DOM 元素或自界说组件上。

我们可以通过引入单独的 display.css:import 'element-ui/lib/theme-chalk/display.css';复制代码它包罗的类名及其寄义如下:hidden-xs-only - 当视口在 xs 尺寸时隐藏hidden-sm-only - 当视口在 sm 尺寸时隐藏hidden-sm-and-down - 当视口在 sm 及以下尺寸时隐藏hidden-sm-and-up - 当视口在 sm 及以上尺寸时隐藏hidden-md-only - 当视口在 md 尺寸时隐藏hidden-md-and-down - 当视口在 md 及以下尺寸时隐藏hidden-md-and-up - 当视口在 md 及以上尺寸时隐藏hidden-lg-only - 当视口在 lg 尺寸时隐藏hidden-lg-and-down - 当视口在 lg 及以下尺寸时隐藏hidden-lg-and-up - 当视口在 lg 及以上尺寸时隐藏hidden-xl-only - 当视口在 xl 尺寸时隐藏我们来看一下它的实现,看一下 display.scss:.hidden { @each $break-point-name, $value in $--breakpoints-spec { &-#{$break-point-name} { @include res($break-point-name, $--breakpoints-spec) { display: none !important; } } }}复制代码实现很简朴,对 $--breakpoints-spec 遍历,生成对应的 CSS 规则,$--breakpoints-spec 界说在 pacakges/theme-chalk/src/common/var.scss 中:$--breakpoints-spec: ( 'xs-only' : (max-width: $--sm - 1), 'sm-and-up' : (min-width: $--sm), 'sm-only': "(min-width: #{$--sm}) and (max-width: #{$--md - 1})", 'sm-and-down': (max-width: $--md - 1), 'md-and-up' : (min-width: $--md), 'md-only': "(min-width: #{$--md}) and (max-width: #{$--lg - 1})", 'md-and-down': (max-width: $--lg - 1), 'lg-and-up' : (min-width: $--lg), 'lg-only': "(min-width: #{$--lg}) and (max-width: #{$--xl - 1})", 'lg-and-down': (max-width: $--xl - 1), 'xl-only' : (min-width: $--xl),);复制代码我们以 xs-only 为例,编译后生成的 CSS 规则如下:.hidden-xs-only { @media only screen and (max-width:767px) { display: none !important; }}复制代码本质上还是使用媒体查询界说了这些 CSS 规则,实现了在某些屏幕尺寸下隐藏的功效。总结其实 Layout 结构还支持了其它一些特性,我纷歧一枚举了,感兴趣的同学可以自行去看。Layout 结构组件充实使用了数据驱动的思想,通过数据去生成对应的 CSS,本质上还是通过 CSS 满足种种灵活的结构。

学习完这篇文章,你应该彻底弄懂 element-ui Layout 结构组件的实现原理,而且对 sass 的 @mixin 以及相关使用到的特性有所相识,对组件实现历程中可以优化的部门,应该有自己的思考。把不会的工具学会了,那么你就进步了,如果你以为这类文章有资助,也接待把它推荐给你身边的小同伴。


本文关键词:AG真人国际厅,Element-UI,技术,揭秘,—,Layout,结构,组件,的

本文来源:AG真人国际厅-www.xdcypx.com