Element-ui Layout布局 Row、Col组件的实现

2,241 阅读4分钟

我们在实际开发中遇到一些布局的时候会用到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主要是为了设置每一列的长度以及偏移量。主要的属性是spanoffset;同样这个组件也是采用render函数去编写,首先我们看如何通过spanoffset去控制列的,源码如下:
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中的媒体查询来进行响应式布局,相应尺寸分别是xssmmdlgxl。使用代码如下:
<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-getmap-get($map,$key)函数的作用就是可以通过$key取到对应的value值,可以理解为就是一个映射关系。如果不存在则不会编译对应的css。举个🌰:

$social-colors: (
    dribble: #ea4c89,
    facebook: #3b5998,
    github: #171515,
    google: #db4437,
    twitter: #55acee
);
.btn-dribble{
  color: map-get($social-colors,facebook);
}
// 编译后
.btn-dribble {
  color: #3b5998;
}

第二个是sass内置方法inspect(value),这个方法就是一个返回一个字符串的表示形式,value是一个sass表达式。举个🌰:

$--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)
);
@mixin res($breakpoint){
  $query:map-get($--breakpoints,$breakpoint)
  @if not $query {
    @error 'No value found for `#{$breakpoint}`. Please make sure it is 
    defined in `$breakpoints` map.';
  }
  @media #{inspect($query)}
   {
    @content;
   }
}
.element {
  color: #000;
 
  @include res(sm) {
    color: #333;
  }
}
// 编译后的css

.element {
  color: #000;
}
@media (min-width: 768px) {
  .element {
    color: #333;
  }
}

好了,我相信聪明的你已经很好的掌握了这两个方法,那我们去看一下element是怎么去实现的吧。
其实上述的第二个例子已经道出一二,我们看一下源码:

$--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)
);
/* Break-points
 -------------------------- */
@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}`";
  }
}
@include res(xs) {
  @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%;
    }
  }
}
@include res(sm) {
  ...
}
@include res(md) {
  ...
}
@include res(lg) {
  ...
}
@include res(xl) {
  ...
}

这样我们就会在不同的屏幕尺寸下进行不同的长度以及间隔的展示了,这样去写我们的媒体查询是不是很棒呢?
好了,本次揭晓了element的栅格布局的面纱,期待我们揭开更多的不为人知的面纱。