Element-plus 学习源码系列 --Layout 布局组件

3,780 阅读5分钟

前言

这段时间在学习前端各个模块的知识点。但在学习的过程中发现缺少整理和思考,事后可能因为知识点掌握不牢固而忘了。所以我希望通过梳理成文章记录自己学习的过程来加深自己的理解。

想写关于 Element-plus 源码的内容主要有两个原因:

  1. vue-next v3.0.0-beta.1 版本在 2020-5-17 发布,距离现在也有一年多了。作为平时主要使用 Vue 技术栈的同学,有必要学习如何更好的使用它,所以在学习 Element-plus 源码的过程中也是对 vue-next 使用的学习。
  2. 在平时开发过程中我们也会常常写一些组件,对比自己写的组件和社区中优秀项目的组件区别,参考和学习设计组件的一些技巧。

本文主要是自己学习的记录和总结,文中有理解错误的地方希望能在评论区指正,谢谢!

准备工作

  1. 既然要学习源码,首先我们 clone element-plus 代码。
  2. 查看 package.json 我们可以通过 script 中的 website-dev 脚本启动项目
  • website/webpack.config.js webpack 配置文件中可以看到,默认已 website/entry.js 作为入口,会加载所有的组件,项目启动比较浪费时间。
  • 执行 npm run website-dev:play,将 website/play.js 作为入口。
  • 我们只需要在 website/play 文件夹下添加 index.vue 文件,这里我们要调试 Layout 组件,就可以复制官网 demo 的例子在这里
  <template>
    <el-row>
      <el-col :span="12"><div class="grid-content bg-purple"></div></el-col>
      <el-col :span="12"><div class="grid-content bg-purple-light"></div></el-col>
    </el-row>
  </template>
  1. 启动项目后,可以在浏览器 Sources > Page > webpack-internal:// > packages 对应的组件文件中设置断点。进行 debug 。 image.png

Layout 组件

通过基础的 24 分栏,迅速简便地创建布局

首先 Layout 实现分为两个组件 ElRowElCol,基础使用方式如下,

<el-row>
  <el-col :span="8"><div class="grid-content bg-purple"></div></el-col>
  <el-col :span="8"><div class="grid-content bg-purple-light"></div></el-col>
  <el-col :span="8"><div class="grid-content bg-purple"></div></el-col>
</el-row>

实现了哪些功能点

  1. 一个ElRow 就是一行,水平空间等分为 24 份,通过给 ElCol 设置 span 属性设置[0,24]的值来定义一个 ElCol 所占的比例。
  2. 通过给 ElRow 设置 gutter 属性来设置分栏间隔的大小
  3. 通过给 ElCol 设置 offset [0,24]属性来设置当前这个 ElCol 相对于左侧的偏移量。同样设置的是相对于 24 的百分比。
  4. 通过给 ElRow 设置 justify 属性设置水平对齐方式,可选值就是 justify-content 的可选值。
  5. 响应式布局,预设了五个响应尺寸:xs、sm、md、lg 和 xl ,在不同屏幕宽度下所占的比例。

接下来逐一来看这些功能点。

24 分栏

// row.ts
import { h } from 'vue'
  setup(props, { slots }) {
    return () => 
        h(props.tag,
        {
          class: [
            'el-row',
            // ...
          ]
          // ...
        },
        slots.default?.(),
        )
  }
  • packages/col/src/col.ts 中,使用 computed 函数动态生成 classList(一个数组),然后将这个类名数组绑定在元素上。ElCol 作为 ElRow 的 slots 所以每一个 ElCol 就是一个 flex-item 。
  • 根据 props.span 的值生成 el-col-${props['span']}的类名。
    • packages/theme-chalk/src/col.scss 中预先定义了 类名。
    @for $i from 0 through 24 {
      .#{$namespace}-col-#{$i} {
        max-width: (math.div(1, 24) * $i * 100) * 1%;
        flex: 0 0 (math.div(1, 24) * $i * 100) * 1%;
      }
    }
    
import { defineComponent, computed, inject, h } from 'vue'
const ElCol = defineComponent({
  props:{
    span: {
      type: Number,
      default: 24,
    },
  },
  setup(props, { slots }) {
    const classList = computed(() => {
      const ret: string[] = []
      const pos = ['span', 'offset', 'pull', 'push'] as const
      pos.forEach(prop => {
        const size = props[prop]
        if (typeof size === 'number') {
          if(prop === 'span') ret.push(`el-col-${props[prop]}`)
        }
      })
    return ret
        return () => h(
        props.tag,
        {
          class: ['el-col', classList.value],
          // ...
        },
        slots.default?.(),
    })
  }
})

gutter

设置分栏之间的间隔

  • 要给每个 flex-item 设置间隔,给每个 flex-item 设置 padding-leftpadding-rightimage.png
  • 此时我们不需要首尾的间隔,在 ElRow 容器元素上设置左右的 margin 负值,效果如下 image.png

了解原理后来看下具体的代码实现。

  • 通过 provide 函数将 gutter 值注入到所有子组件中。
  • ElRow 组件中要做的就是去除首尾的间隔,大小为 -gutter/2 。因为每列都是在左右增加 gutter/2 大小的 padding。
    • 使用 computed 函数计算出 style 对象。将 style 对象动态绑定到元素的 style 属性上。
export default defineComponent({
  name: 'ElRow',
  props: {
    gutter: {
      type: Number,
      default: 0,
    },
    // ...
  }
  setup(props, { slots }) {
    // 返回 ComputedRef 对象
    const gutter = computed(() => props.gutter)
    provide('ElRow', {
      gutter,
    })
    const style = computed(() => {
      const ret = {
        marginLeft: '',
        marginRight: '',
      }
      if (props.gutter) {
        ret.marginLeft = `-${props.gutter / 2}px`
        ret.marginRight = ret.marginLeft
      }
      return ret
    })
    return () =>
      h(
        props.tag,
        {
          style: style.value,
        },
        slots.default?.(),
      )
  }
  • 使用 inject 函数接受父级组件(不管几层嵌套,是父级别还是爷爷级别的)注入的属性。因为 provide 的时候是 computed 的返回值,是一个 Ref 对象,所以这里的默认值设置的也是包含一个 value 的对象。
  • 同样计算出一个 style 属性,绑定到元素的 style 上。
setup(props,{slots}){
  const { gutter } = inject('ElRow', { gutter: { value: 0 } })
  const style = computed(() => {
    if (gutter.value) {
      return {
        paddingLeft: gutter.value / 2 + 'px',
        paddingRight: gutter.value / 2 + 'px',
      }
    }
    return {}
  })
  return () => h(
    props.tag,
    {
      // ...
      style: style.value,
    },
    slots.default?.(),
  )
}

offset

offset 设置和 24 分栏原理一致。

      const pos = ['span', 'offset', 'pull', 'push'] as const
      pos.forEach(prop => {
        const size = props[prop]
        if (typeof size === 'number') {
          if(prop === 'span') ret.push(`el-col-${props[prop]}`)
          else if(size > 0) ret.push(`el-col-${prop}-${props[prop]}`)
        }
      })
  1. 接受 offset props,根据 props.offset 的大小动态生成 el-col-offset-${props['offset']} 的类名,并绑定到动态的 class 属性上。
  2. 这些预设的类名定义在 packages/theme-chalk/src/col.scss
@for $i from 0 through 24 {
  .#{$namespace}-col-offset-#{$i} {
    margin-left: (math.div(1, 24) * $i * 100) * 1%;
  }
}
  1. 当你设置了其中的类名就可以应用对应的 margin-left 的偏移量了。

justify

相当于给 flex 容器设置 justify-content 属性,样式定义在 packages/theme-chalk/src/row.scss 中。

响应式布局

支持在不同屏幕尺寸下的栅格数或者栅格属性对象

五个响应尺寸:xs、sm、md、lg 和 xl。下可以设置不同的栅格数(所占比例)。支持对象或者数字。

    <el-row>
      <el-col :xs="{span: 4, offset: 2}" :sm="10"><div class="grid-content bg-purple"></div></el-col>
    </el-row>

来看下代码中是如何实现的。

  1. 首先定义了 ['xs', 'sm', 'md', 'lg', 'xl'] 类型,然后遍历这些 props 传递的是哪种类型。
  2. 如果是 number 类型,直接添加 el-col-xs-10 格式的类名。
  3. 如果是对象类型如 {span:4,offset:2},将该对象的下所有的 key 取出,添加类名 el-col-xs-offset-2 将 props.span值设置到 el-col-xs-4 类名中。
  4. 类名定义在 packages/theme-chalk/src/col.scss 中使用了 packages/theme-chalk/src/mixins.scss 定义 res mixins。
  • 不同屏幕尺寸的大小变量定义在 packages/theme-chalk/src/common/var.scss$--breakpoints css变量。
  setup(props, { slots }) {
    const classList = computed(() => {
      const ret: string[] = []
      const sizes = ['xs', 'sm', 'md', 'lg', 'xl'] as const
      sizes.forEach(size => {
        if (typeof props[size] === 'number') {
          ret.push(`el-col-${size}-${props[size]}`)
        } else if (typeof props[size] === 'object') {
          const sizeProps = props[size]
          Object.keys(sizeProps).forEach(prop => {
            ret.push(
              prop !== 'span' ? `el-col-${size}-${prop}-${sizeProps[prop]}` : `el-col-${size}-${sizeProps[prop]}`,
            )
          })
        }
      })
    }
    return () => h(
      props.tag,
      {
        class: ['el-col', classList.value],
      },
      slots.default?.(),
    )
  }

总结

  • 主要涉及到vue-next 语法有以下几点: setuph 渲染函数语法computedprovideinject
  • 分栏、gutter 等功能实现都是通过预先设定一些类名。然后根据 props 传递到值来动态生成类名。如果匹配到就使用对应的样式。
    • 动态类名使用主要有一下几种方式。绑定字符串、对象、数组。
    • 动态类名的拼接过程中有些情况下需要拼接 key 或者 value,可以灵活的调整,使得代码更加简介和灵活
<div
        :class="[
        ['class-1','class-2'],
        {'class-3':true},
        native ? '' : 'scrollbar__wrap--hidden-default'
      ]"
     />