浅析element plus是如何组织CSS的:BEM最佳实践?
一、文章包括的内容:
- button组件如何配置样式的
- scss文件如何配置样式的
- 命名空间如何运用到UI框架当中的
二、背景
- 接受到一个任务,需要做一个element plus的自定义主题。看了一下官网,步骤清楚,就是一点看不懂。而且官网的仅仅是对于主题色的配置,如何对样式进行扩展没有例子。看得我一脸懵逼,所以有必要研究一下element plus对于样式是如何组织实现的。
- 不看Element plus 的源码,都不知道现在写样式都这么复杂了。一眼看过去和我以前学习的css相去甚远。我以前主要负责的是原生APP的开发,要不是实在是找不到工作,都想要立马跑路,滚回去做APP了。想想那些投完简历未读的消息,还是算了。
三、源码分析
3.1 阅读的方法论
- 第一件事情,就是去查看了官方文档,总大体上面知道它有个什么东西,实现什么功能。这个无需多说。
- 第二件事情要做的就是找好自己的目的。正如这次我的目的是看element是如何组织CSS的。这个我们可以最常用的button组件来进行突破。阅读源码很忌泛读!
- 第三件事情,搞明白项目的结构,什么文件夹放什么东西(由于不是本次的重点,本篇文章略过)
3.2 button.vue的class组织逻辑
直接找到buttom组件文件夹,该组件位于components/button/button.vue中:

都是啥命名,这么简洁的么?ns? b? m? 对于UI组件库来说,这些算是它们的业务命名了么?所以说学习怎么命名不能抄UI组件库的。
不管命名是什么意思,既然是在"class" 当中,它们返回的一定是class类名。往下查看:
const ns = useNamespace('button')
"ns=useNamespace" 原来是取之命名空间的意思,大胆猜测参数是当前组件的名字。
继续查看该方法,位于"hooks/use-namespace" 中,代码很简单,是一些拼接字符串的方法,基本结构如下:
const defaultNamespace = 'el' // 这就是默认的空间命名前缀了
export const useNamespace = (block: string) => {
const namespace = useGlobalConfig('namespace', defaultNamespace)
const b = (blockSuffix = '') =>
_bem(namespace.value, block, blockSuffix, '', '')
// code ...
return {
namespace,
b,
m,
...
// css
cssVar
....
}
}
这部分代码有两个核心:
- 获取class的前缀命名
- 拼接节点需要的class名
获取class的前缀
核心代码是 const namespace = useGlobalConfig('namespace', defaultNamespace)
。
找到位于use-global-config的代码
export function useGlobalConfig(
key?: keyof ConfigProviderContext,
defaultValue = undefined
) {
const config = getCurrentInstance()
? inject(configProviderContextKey, globalConfig)
: globalConfig
if (key) {
return computed(() => config.value?.[key] ?? defaultValue)
} else {
return config
}
}
Element plus官网指南中,用于自定义命名空间的方法如下:
<!-- App.vue -->
<template>
<el-config-provider namespace="ep">
<!-- ... -->
</el-config-provider>
</template>
- 而上面代码的中, 获取自定义命名空间可以值得,它是通过provider/inject,即依赖注入的方式来拿到用的自定义命名的。
- 如果拿不到,就拿"defalutValue" 。这个值从上文可知,就是我们所熟知的"el" 。
以上就是element plus 命名空间的实现原理。
拼接节点的class名
有一个很有意思的方法名,一下就让我明白了那些b?m?的命名的含义,那就是"_bem" ,哦,element plus的class的命名规范是严格按照BEM来的。
看一下"_bem" 的参数就更加的明显了。
const _bem = (
namespace: string,
block: string,
blockSuffix: string,
element: string,
modifier: string
): string {
// ...
let cls: string = `${namespace}-${block}`
}
- 典型的块(Block)、元素(Element)、修饰符(Modifier)。在"vue" 文件中,通过传进去的参数来拼接完整"BEM" 规范的class名。
- 而多出来而"namespace" 和"blckSuffix" 。前者是命名的空间,是为了解决样式污染的问题,2.2又更进一步的提供了自定义空间命名,可以说是针对微前端实现样式隔离的重器。这个是element根据实际的业务需求进行扩展的。比如说"el-button-group" 中的"group" 。button包括两种类型的,单按钮和按钮组,这就是它的业务,所以element也并没有死板的使用BEM,而是根据实际的需求扩展了它的定义。
由此可以猜测,element plus在写之前就已经确定了各个组件的命名规范,以及各自的语义。
总结一下
- 通过依赖注入来实现namespace的自定义功能,既自定义命名空间。
- 根据实际的需求扩展了BEM的定义,通过"_bem"函数来拼接最终的class命名,提高BEM使用效率。
BEM存在的意义,或者说任何规范存在的意义,都是让多人协作的时候降低理解对方代码的时间成本。降低来回沟通的成本。对于这一点我更加激进一点,我认为自己一个人写的时候也应该遵守一个规范。可能是我的记忆不太好,常常把一个月前自己写的代码意思给忘记,但是如果自己一直都遵循着一个规范,自然会降低自己回忆的心智负担。现在的自己不认识一个月前的自己,一个月前的自己不认识两一个月前的自己,所以说,即便表面是一个人的项目,本质还是自己和跨时空的自己进行协作。
3.3 scss对于BEM的实现
组件上面挂载的class已经确定,scss的自然也要对应得上。为了方便管理,以及提供给用户自定义主题,这部分是作为一个独立项目存在的。
直接到"theme-chalk/src/button.scss" 文件中:
@use 'common/var' as *; // 放了样样式的常量,包括各种主题色,字体大小
@use 'mixins/button' as *; // 单按钮button和按钮组button-group的共性样式
@use 'mixins/mixins' as * // class的BEM规范拼接,CSS值的拼接
@use 'mixins/utils' as *; // 工具类共性
@use 'mixins/var' as *; // css值的常量拼接,比如--el-color-primary这样的主题色名
看到mixin就害怕,使用@use { b, m } 'mixins'这种方式多好!起码可以让我一眼可以看出来当前文件出自哪个地方。也就是方便溯源。满屏的mixins
怪吓人的。往下看具体的应用:
// button.scss
@include b(button) {
// ...
height: map.get(@input-height, 'default')
color: getCssVar('button', 'text-color'),
}
其中b()对于的mixin位于mixins/mixins.scss当中:
@mixin b($block) {
$B: $namespace + '-' + $block !global
.#{$B} {
@content;
}
}
这两部分涉及到的知识点如下:
@include和@mixin: @mixin b方法抽象出了class命名的方法,并且使用类似插槽的@content来把@include b中的样式给放入
map.merge和map.get:
scss的基本语法。上面代码片段中的@input-height来源于"common/var" 之中
$common-component-size: () !default;
$common-component-size: map.merge(
(
'large': 40px,
'default': 32px,
'small': 24px,
),
$common-component-size
);
$input-height: () !default;
$input-height: map.merge($common-component-size, $input-height);
@inptu-height合并了@common-component-size上面的集合
随后通过map.get(@input-height, 'default')
来取值,等于height: 32px
@function和joinVarNmae()
scss中的函数,用法和JS差不多,不同的是前面都有一个@符号。实现代码如下:
// joinVarName(('button', 'text-color')) => '--el-button-text-color'
@function joinVarName($list) {
$name: '--' + config.$namespace;
@each $item in $list {
@if $item != '' {
$name: $name + '-' + $item;
}
}
@return $name;
}
@function getCssVar($args...) {
@return var(#{joinVarName($args)});
}
我们传入的参数是getCssVar('button', 'text-color')
,即它拼接返回的属性值为--el-button-text-color
。
其他的就大同小异了,再此不过多做分析。
四、总结
-
element plus的样式组织分为两部分。
- 第一部分是DOM上面的class名,第二部分是主题项目当中的scss名字
- 只有两者对上了,样式就能够作用于组件库当中。
-
vue组件的自定义空间是通过依赖注入来实现的。
-
scss文件的自定义空间是通过scss带来的"@foward" 复写"element-plus/theme-chalk/src/mixins/config.scss" 中的"@namespace" 来实现的。
不看Element plus 的源码,都不知道现在写样式都这么复杂了。一眼看过去和我以前学习的css相去甚远。我以前主要负责的是原生APP的开发,要不是实在是找不到工作,都想要立马跑路,滚回去做APP了。想想那些投了简历而未读的列表消息,还是算了。
转载自:https://juejin.cn/post/7200669128289697847