Element UI 组件源码分析之 Layout (栅格化)布局系统

2,494 阅读3分钟

0x00 简介

组件提供了布局的栅格化(Grid Layout)系统,通过基础的 24 分栏,迅速简便地创建布局。该系统使用Row 和 Col 栅格组件进行创建。本文将深入分析源码,剖析其实现原理,耐心读完,相信会对您有所帮助。

🔗 组件文档 Layout 🔗 gitee源码 行组件row.js 🔗 gitee源码 列组件col.js

更多组件分析详见 👉 📚 Element UI 源码剖析组件总览

本专栏的 gitbook 版本地址已经发布 anduril.gitbook.io/learning-el… ,内容同步更新中!


0x01 Grid Layout

栅格化提高了页面布局的一致性跟复用性,提升了整个设计开发流程的效率。同时使得网页的布局更加规范和简洁,提升用户体验。

网页栅格化神器 Grid.Guide ,可以自由设置最大宽度、列数以及留白边界自动动生成多种最佳栅格方案以供选择。

image.png

Bootstrap 提供了一套响应式、移动设备优先的流式栅格系统,随着屏幕或视口(viewport)尺寸的增加,系统会自动分为最多12列。

element 2 采用 Ant Design 的设计理念:在 12 栅格系统的基础上,将整个设计建议区域按照 24 等分的原则进行划分,解决在设计区域内大量信息收纳的问题。


栅格化布局系统 通过一系列的行(row)和列(col)来定义信息区块的外部框架,以保证页面的每个区域能够稳健地排布起来。下面就介绍一下栅格系统的工作原理:

  • 通过 row 在水平方向建立一组 column(简写 col)。
  • 内容应当放置于 col 内,并且只有 col 可以作为 row 的直接元素。
  • 栅格系统中的列是指 1 到 24 的值来表示其跨越的范围。例如,三个等宽的列可以使用 <col :span="8" /> 来创建。
  • 如果一个 row 中的 col 总和超过 24,那么多余的 col 会作为一个整体另起一行排列。

接下来对组件行(row)和列(col)一一进行讲解。

0x02 Row 行组件

packages/row/src/row.js 使用渲染函数构建组件,支持组件渲染成自定义元素标签,主要用来作为col的容器。

render()

组件根据指定自定义元素渲染标签节点,由组件 prop 属性值动态计算添加 class 和 自定义样式(计算属性style),内部提供一个匿名插槽用于分发内容。

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);
}

JSX 语法中,h 作为 createElement 的别名。第二个参数是一个包含模板相关属性的数据对象VNodeData,对象属性如下。

{
  // 与 `v-bind:class` 的 API 相同,
  // 接受一个字符串、对象或字符串和对象组成的数组
  'class': {
    foo: true,
    bar: false
  },
  // 与 `v-bind:style` 的 API 相同,
  // 接受一个字符串、对象,或对象组成的数组
  style: {
    color: 'red',
    fontSize: '14px'
  },
  // ...
}

若自定义元素标签为<div>,等同于如下 template 实现。

<template>
  <div 
    :style="style"
    :class="[
      'el-row',
      justify !== 'start' ? 'is-justify-' + justify : '',
      align !== 'top' ? 'is-align-' + align : '',
      { 'el-row--flex': type === 'flex'   }
    ]"
  >
    <slot></slot>
  </div>
</template>

组件 props

组件定义了 5 个 prop : tag 、guttertypejustifyalign

tag

支持组件渲染成自定义html标签,默认值为 div, 作为 createElement 方法的第一个参数。

props: { 
  tag: {
    type: String,
    default: 'div'
  },
},

gutter

栅格间隔设置,用来指定每一栏之间的间隔,默认间隔为 0。col 组件通过获取父组件rowgutter 计算自己的左右 padding 。

props: { 
  gutter: Number, 
},

Flex 布局设置

type 设置布局模式,可选 flex,仅在现代浏览器下有效。

props: { 
  type: String,
},

type 值为flex{ 'el-row--flex': type === 'flex' } 会添加 class el-row--flex,开启flex 布局。

.el-row--flex { 
  display: flex;
}

justify 用于设置flex 布局下的水平排列方式,可选值start/end/center/space-around/space-between,生成的样式 is-justify-[justify],用于设置 justify-content 属性。 其他值生成的样式无效。

align 用于设置flex 布局下的垂直排列方式,可选值top/middle/bottom,生成的样式 is-align-[align],用于设置 align-items 属性。 其他值生成的样式无效。

props: { 
  // flex 布局下的水平排列方式  justify-content
  justify: {
    type: String,
    default: 'start'
  },
  // flex 布局下的垂直排列方式  align-items
  align: {
    type: String,
    default: 'top'
  }
},

元素默认布局不会生成 flex 样式 justify !== 'start' ? 'is-justify-' + justify : '', align !== 'top' ? 'is-align-' + align : '',

系统基于 Flex 布局,允许子元素在父节点内的水平对齐方式 - 居左、居中、居右、等宽排列、分散排列。子元素与子元素之间,支持顶部对齐、垂直居中对齐、底部对齐的方式。

Flex 布局,可以简便、完整、响应式地实现各种页面布局。其语法本文不做过多赘述,详见 “Flex 布局教程”,阮一峰

计算属性

计算属性 style 通过为 row 组件设置负值 margin 从而抵消掉为 col 组件设置的 padding,也就间接为“行(row)”所包含的“列(column)”抵消掉了padding

computed: {
  style() {
    const ret = {}; 
    // 通过gutter计算出实际左右 margin
    if (this.gutter) {
      ret.marginLeft = `-${this.gutter / 2}px`;
      ret.marginRight = ret.marginLeft;
    } 
    return ret;
  }
},

0x03 Col 列组件

packages/col/src/col.js 使用渲染函数构建组件,支持渲染自定义元素标签。

render()

组件根据指定自定义元素渲染标签节点,由组件 prop 属性值动态计算添加 class 和 自定义样式,内部提供一个匿名插槽用于分发内容。

render(h) {
    let classList = [];
    let style = {};
    
    // sytle 计算
    // class 计算 

    return h(this.tag, {
      class: ['el-col', classList],
      style
    }, this.$slots.default);
  }

若自定义元素标签为<div>,等同于如下 template 实现。

<template>
  <div 
    :style="style"
    :class="['el-col', classList]"
  >
    <slot></slot>
  </div>
</template>

组件 props

定义了10 个 prop ,具体功能详见中文注释。 span 默认值 24,对应栅格系统24分栏。

props: {
  // 自定义元素标签
  tag: {
    type: String,
    default: 'div'
  },
  // 栅格占据的列数,总共24列,如果设置为0,则不渲染
  span: {
    type: Number,
    default: 24
  }, 
  // 栅格左侧的间隔格数
  offset: Number,
  // 栅格向右移动格数
  pull: Number,
  // 栅格向左移动格数
  push: Number,
  // 响应式布局设置
  // 响应式栅格数或者栅格属性对象 number/object (例如: {span: 4, offset: 4})
  // xs <768px  sm ≥768px  md ≥992px  lg ≥1200px  xl ≥1920px
  xs: [Number, Object],
  sm: [Number, Object],
  md: [Number, Object],
  lg: [Number, Object],
  xl: [Number, Object]
},

计算属性

计算属性gutter 获取父组件 rowgutter值 。通过 row 组件的自定义 property 进行判断parent.$options.componentName !== 'ElRow'

computed: { 
  gutter() {
    // 获取父实例 根据 compontName 属性 判断是组件 row
    let parent = this.$parent;
    while (parent && parent.$options.componentName !== 'ElRow') {
      parent = parent.$parent;
    }
    return parent ? parent.gutter : 0;
  }
},

// packages\row\src\row.js
export default { 
  name: 'ElRow',
  // 自定义 property  
  componentName: 'ElRow',
}

组件 padding

若计算属性gutter值不为 0,计算 col 的左右 padding。

render(h) { 
    let style = {}; 
    // sytle 计算
    if (this.gutter) {
      style.paddingLeft = this.gutter / 2 + 'px';
      style.paddingRight = style.paddingLeft;
    } 
  }

组件 class

分栏、间隔、左右偏移

栅格、间隔、左右偏移的样式动态计算。

// span 栅格占据的列数,通过 width 来实现
// offset 栅格左侧的间隔格数,通过 margin-left 实现
// push 栅格向右移动格数,通过 left 实现
// pull 栅格向左移动格数,通过 right 实现 
['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]}`
    );
  }
});

col组件样式 scss 实现。 由.el-col-0可知,当 span 设置为0时,组件 display 值为none ,不会被渲染。

[class*="el-col-"] {
  float: left;
  // 如何计算一个元素的总宽度和总高度
  box-sizing: border-box;
}
// 组件不渲染
.el-col-0 {
  display: none;
}

@for $i from 0 through 24 {
  // 生成 .el-col-0,.el-col-1, ... ,el-col-24
  .el-col-#{$i} {
    width: (1 / 24 * $i * 100) * 1%;
  }
  // 生成 .el-col-offset-0,.el-col-offset-1, ... ,el-col-offset-24
  .el-col-offset-#{$i} {
    margin-left: (1 / 24 * $i * 100) * 1%;
  }
  // 生成 .el-col-pull-0,.el-col-pull-1, ... ,el-col-pull-24
  .el-col-pull-#{$i} {
    position: relative;
    right: (1 / 24 * $i * 100) * 1%;
  }
  // 生成 .el-col-push-0,.el-col-push-1, ... ,el-col-push-24
  .el-col-push-#{$i} {
    position: relative;
    left: (1 / 24 * $i * 100) * 1%;
  }
}

响应式布局

响应式布局样式动态计算。预设四个响应尺寸:xs sm md lg
传入数字的话只会影响 span,还可以传入对象 {span: 4, offset: 4} ,属性可选范围 span/offset/pull/push

// xs <768px 响应式栅格数或者栅格属性对象
// sm ≥768px 响应式栅格数或者栅格属性对象
// md ≥992px 响应式栅格数或者栅格属性对象
// lg ≥1200px 响应式栅格数或者栅格属性对象 
// xl ≥1920px 响应式栅格数或者栅格属性对象 
['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]}`
      );
    });
  }
});

使用指令 res 生成 @media 媒体查询样式,其scss实现如下:

// 'xs', 'sm', 'md', 'lg', 'xl' 
// 生成 @media only screen and (min-width: xxx px) 
@include res(sm) {
  // 生成  .el-col-sm-0
  .el-col-sm-0 {
    display: none;
  }
  @for $i from 0 through 24 {
    // 生成 .el-col-sm-0,.el-col-sm-1, ... ,el-col-sm-24
    .el-col-sm-#{$i} {
      width: (1 / 24 * $i * 100) * 1%;
    }
    // 生成 .el-col-sm-offset-0,.el-col-sm-offset-1, ... ,el-col-sm-offset-24 
    .el-col-sm-offset-#{$i} {
      margin-left: (1 / 24 * $i * 100) * 1%;
    }
    // 生成 .el-col-sm-pull-0,.el-col-sm-pull-1, ... ,el-col-sm-pull-24
    .el-col-sm-pull-#{$i} {
      position: relative;
      right: (1 / 24 * $i * 100) * 1%;
    }
    // 生成 .el-col-sm-push-0,.el-col-sm-push-1, ... ,el-col-sm-push-24
    .el-col-sm-push-#{$i} {
      position: relative;
      left: (1 / 24 * $i * 100) * 1%;
    }
  }
}

📚参考&关联阅读

“渲染函数 & JSX”,vuejs.org
“@media”,MDN

关注专栏

如果本文对您有所帮助请关注➕、 点赞👍、 收藏⭐!您的认可就是对我的最大支持!

此文章已收录到专栏中 👇,可以直接关注