likes
comments
collection
share

浅析element plus是如何组织CSS的:BEM最佳实践?

作者站长头像
站长
· 阅读数 17

一、文章包括的内容:

  • 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中:

浅析element plus是如何组织CSS的:BEM最佳实践?

浅析element plus是如何组织CSS的:BEM最佳实践?

都是啥命名,这么简洁的么?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了。想想那些投了简历而未读的列表消息,还是算了。