前言
这段时间在学习前端各个模块的知识点。但在学习的过程中发现缺少整理和思考,事后可能因为知识点掌握不牢固而忘了。所以我希望通过梳理成文章记录自己学习的过程来加深自己的理解。
想写关于 Element-plus 源码的内容主要有两个原因:
vue-next v3.0.0-beta.1版本在 2020-5-17 发布,距离现在也有一年多了。作为平时主要使用Vue技术栈的同学,有必要学习如何更好的使用它,所以在学习Element-plus源码的过程中也是对vue-next使用的学习。- 在平时开发过程中我们也会常常写一些组件,对比自己写的组件和社区中优秀项目的组件区别,参考和学习设计组件的一些技巧。
本文主要是自己学习的记录和总结,文中有理解错误的地方希望能在评论区指正,谢谢!
准备工作
- 既然要学习源码,首先我们 clone element-plus 代码。
- 查看
package.json我们可以通过script中的website-dev脚本启动项目
- 在
website/webpack.config.jswebpack 配置文件中可以看到,默认已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>
- 启动项目后,可以在浏览器
Sources > Page > webpack-internal:// > packages对应的组件文件中设置断点。进行 debug 。
Layout 组件
通过基础的 24 分栏,迅速简便地创建布局
首先 Layout 实现分为两个组件 ElRow 和 ElCol,基础使用方式如下,
<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>
实现了哪些功能点
- 一个
ElRow就是一行,水平空间等分为24份,通过给ElCol设置 span 属性设置[0,24]的值来定义一个 ElCol 所占的比例。 - 通过给
ElRow设置 gutter 属性来设置分栏间隔的大小 - 通过给
ElCol设置 offset [0,24]属性来设置当前这个ElCol相对于左侧的偏移量。同样设置的是相对于 24 的百分比。 - 通过给
ElRow设置 justify 属性设置水平对齐方式,可选值就是justify-content的可选值。 - 响应式布局,预设了五个响应尺寸:xs、sm、md、lg 和 xl ,在不同屏幕宽度下所占的比例。
接下来逐一来看这些功能点。
24 分栏
-
在
packages/col/src/row.ts中,在 setup 返回渲染函数,不熟悉该语法可以参考官方文档,此处不多解释 -
渲染函数中可以动态创建标签名,并且设置类名
el-row,将元素设置为 flex 容器。 -
slots.default 类型为函数
slots.default?.()使用 可选链操作符
// 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-left 和 padding-right 。
- 此时我们不需要首尾的间隔,在
ElRow容器元素上设置左右的 margin 负值,效果如下
了解原理后来看下具体的代码实现。
- 通过 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]}`)
}
})
- 接受
offsetprops,根据 props.offset 的大小动态生成el-col-offset-${props['offset']}的类名,并绑定到动态的 class 属性上。 - 这些预设的类名定义在
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%;
}
}
- 当你设置了其中的类名就可以应用对应的 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>
来看下代码中是如何实现的。
- 首先定义了
['xs', 'sm', 'md', 'lg', 'xl']类型,然后遍历这些 props 传递的是哪种类型。 - 如果是 number 类型,直接添加
el-col-xs-10格式的类名。 - 如果是对象类型如
{span:4,offset:2},将该对象的下所有的 key 取出,添加类名el-col-xs-offset-2将 props.span值设置到el-col-xs-4类名中。 - 类名定义在
packages/theme-chalk/src/col.scss中使用了packages/theme-chalk/src/mixins.scss定义resmixins。
- 不同屏幕尺寸的大小变量定义在
packages/theme-chalk/src/common/var.scss中$--breakpointscss变量。
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语法有以下几点:setup、h 渲染函数语法、computed、provide、inject等 - 分栏、gutter 等功能实现都是通过预先设定一些类名。然后根据 props 传递到值来动态生成类名。如果匹配到就使用对应的样式。
- 动态类名使用主要有一下几种方式。绑定字符串、对象、数组。
- 动态类名的拼接过程中有些情况下需要拼接
key或者value,可以灵活的调整,使得代码更加简介和灵活
<div
:class="[
['class-1','class-2'],
{'class-3':true},
native ? '' : 'scrollbar__wrap--hidden-default'
]"
/>