目录
- 基本说明以及用法
- Row组件的分析
- render函数
- 源码分析
- Col组件的分析
- 组件分析
- 响应式布局
我们在实际开发中遇到一些布局的时候会用到Layout布局,这个布局只要配置一些参数就能够达到很好的布局效果甚至可以响应式,那里面的具体是怎么实现的呢,让我们去剖开Element-UI的源码,学习里面的一些细节吧。
基本说明以及用法
Element-UI的Layout布局是通过基础的24分栏,迅速简便地创建布局。根据不同的组合,很快的就能够生成一个很美观的响应式布局。具体的用法如下:
<el-row> <el-col :span="24"><div class="grid-content bg-purple-dark"></div></el-col> </el-row>
由上述的示例代码可以看出Row组件主要是创建每行分栏的布局方式,比如之间的一些间隔、对齐方式等。而Col则创建每个分栏,分栏的长度、偏移量等。我们可以进行自由组合每个分栏,从而达到一种响应式效果。
Row组件的分析
render函数
我们都知道vue除了可以使用template模板编写组件外,有时候我们还可以直接使用render函数去编写一个组件。因为template模板最终也是编译成了render函数。
为什么会有render函数的写法?比如现在有个需求:根据动态的level生成从h1-h6字体大小的标题的时候,我们如果使用template去实现的话我们页面中可能会出现很多类似这样的伪代码:
<template> <h1 v-if="level === 1"> <slot></slot> </h1> <h2 v-else-if="level === 2"> <slot></slot> </h2> <h3 v-else-if="level === 3"> <slot></slot> </h3> <h4 v-else-if="level === 4"> <slot></slot> </h4> <h5 v-else-if="level === 5"> <slot></slot> </h5> <h6 v-else-if="level === 6"> <slot></slot> </h6> </template>
但是如果是使用render函数则是比较简单:
Vue.component('anchored-heading', { render: function (createElement) { return createElement( 'h' + this.level, // 标签名称 this.$slots.default // 子节点数组 ) }, props: { level: { type: Number, required: true } } })
这里还有一个代码优化点是。this.$slots.default存储的就是插槽内容,不需要写那么多遍。
源码分析
Row组件的源码比较简单,因为我们可以通过tag这个prop对其指定一个渲染标签,所以组件是直接使用render渲染函数进行编写。 render函数部分如下:
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); }
如上的源码可以得出Row主要是控制class名来进行控制内容布局的。这里有gutter属性能够控制行内列的间隔数。如果说我们设置为gutter=20,那么每个列项都进行左右间距10px,那么就会出现个问题:第一个列项跟最后一个列项会出现左右的间距。那该如何让第一个跟最后一个左右间隔去掉这个10px呢?Row的处理方案是这个行左右各偏-10px,所以用了一个计算属性来设置样式:
computed: { style() { const ret = {}; if (this.gutter) { ret.marginLeft = `-${this.gutter / 2}px`; ret.marginRight = ret.marginLeft; } return ret; } },
Col组件的分析
组件分析
Col主要是为了设置每一列的长度以及偏移量。主要的属性是span、offset;同样这个组件也是采用render函数去编写,首先我们看如何通过span、offset去控制列的,源码如下:
render(h) { let classList = []; let style = {}; ... ['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]}` ); } }); ... return h(this.tag, { class: ['el-col', classList], style }, this.$slots.default); }
从这可以看出,col的列宽是通过不同class名去做控制的。我们找到对应的.scss文件,发现他使用了sass@for循环语句去计算不同格子的宽度:
@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%; } }
同理offset也是使用相同的逻辑。这样我们就可以根据不同的span、跟offset混合组合不同风布局了,是不是感觉背后的逻辑是如此的简单呢。我们再思考一个问题就是如果我们要控制一组相同的列宽间隔,需要一个个的去做设置么?答案是不用的,我们可以借助上述的Row组件中的gutter属性去做统一设置。那怎么实现的呢?源码如下:
computed: { gutter() { let parent = this.$parent; while (parent && parent.$options.componentName !== 'ElRow') { parent = parent.$parent; } return parent ? parent.gutter : 0; } }
我们通过往上遍历父组件,如果父组件的组件名为ElRow,则取到gutter值,然后让组件左右内边距设置对应的值就好了:
if (this.gutter) { style.paddingLeft = this.gutter / 2 + 'px'; style.paddingRight = style.paddingLeft; }
这样我们就解决了统一列宽设置的问题;
响应式布局
这里我们用到了css3中的媒体查询来进行响应式布局,相应尺寸分别是xs、sm、md、lg 和 xl。使用代码如下:
<el-row :gutter="10"> <el-col :xs="8" :sm="6" :md="4" :lg="3" :xl="1"><div class="grid-content bg-purple"></div></el-col> <el-col :xs="4" :sm="6" :md="8" :lg="9" :xl="11"><div class="grid-content bg-purple-light"></div></el-col> <el-col :xs="4" :sm="6" :md="8" :lg="9" :xl="11"><div class="grid-content bg-purple"></div></el-col> <el-col :xs="8" :sm="6" :md="4" :lg="3" :xl="1"><div class="grid-content bg-purple-light"></div></el-col> </el-row>
说明:xs:<768px 响应式栅格数或者栅格属性对象,sm:≥768px 响应式栅格数或者栅格属性对象,md:≥992px 响应式栅格数或者栅格属性对象,lg:≥1200px 响应式栅格数或者栅格属性对象,xl:≥1920px 响应式栅格数或者栅格属性对象.
背后的逻辑就是不同屏幕尺寸所展示的格子数是不一样的,而且是根据屏幕宽度进行响应式。首先,我们看是如何进行不同的class绑定的:
['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]}` ); }); } });
这里面xs等属性也是可以使用对象。所以会有个处理对象的逻辑;以上的js处理的逻辑比较简单,我们再看一下css是怎么处理这个媒体查询的逻辑的。
在分析css的时候,我们先了解一个概念,那就是sass中的内置方法map-get。map-get($map,$key)函数的作用就是可以通过$key取到对应的value值,可以理解为就是一个映射关系。如果不存在则不会编译对应的css。举个