专栏 - 6.CSS架构模式之BEPlus在组件库中的实践

前言

Element Plus 的 CSS 架构并没有脱离传统的 CSS 设计模式,主要还是 OOCSS,也就是还是面向对象的思想,即:封装、继承、多态,并且 CSS 变量的加入使用,使得多态这一特性的实现更加丝滑。

整个 Element Plus 的 CSS 架构核心思想就是,首先将那些公共的 UI 样式进行提取封装成公共 CSS 变量,相当于基础类,然后每个组件又基于公共的 CSS 变量进行继承封装属于每个组件独立的 CSS 变量,相当于子类。这样一旦修改基础类的 CSS 变量,所有继承基础类的组件的样式都会发生改变,这个就是使用 CSS 变量进行封装继承的好处,但如果只想对其中某一个组件的样式做深度的定制,则可以只修改该组件的 CSS 变量,这样就实现了多态

值得注意的是CSS 变量可以在运行时进行更改值,这也使得整个组件库的样式更改变化有了非常大的灵活性以及方便性,这意味着你可以动态地改变组件内的个别变量,就可以更好地自定义组件样式,而不需要修改 SCSS 文件重新编译一次。

此外 Element Plus 默认提供一套主题,CSS 命名采用 BEM 的风格,主要是方便使用者覆盖样式,进行深度定制组件样式。而关于 BEM 我们在本专栏《6. CSS 架构模式之 BEM 在组件库中的实践》的一文中已经进行详细介绍了,本文将不再进行赘述,大家可以自行查看了解。本章节将在前面的文章基础上继续深入研究学习 Element Plus 中的 CSS 架构。

通过 CSS 变量,Element Plus 默认提供了“白天模式”和“夜间模式”又或着叫:暗黑模式两种皮肤。关于暗黑模式,我们已经在上一篇《10. CSS 系统颜色和暗黑模式的关系及意义》文章中已经进行详细探讨过了,这里也不进行赘述了。

我们理解了 Element Plus 的 CSS 架构的这些核心思想之后,我们再去研究和阅读 Element Plus 的 CSS 源码则会有一种豁然开朗的感觉,因为它所做的一切都是基于上述的核心思想,所谓理论指导实践,当然也可以在实践中总结理论。当然除了上述思想外,Element Plus 的 CSS 架构还要实现一些组件特有的业务需求,比如按需加载组件的样式如何处理,这些可以在我们详细了解 Element Plus 的 CSS 架构之后再进行具体学习研究。

什么是 UI 样式和结构样式

我们在前言中说到 Element Plus 会把那些公共的 UI 样式进行提取封装成公共 CSS 变量,那么这里所说的 UI 样式是什么呢?

UI 样式结构样式(或叫:布局样式),这些概念和思想在传统 CSS 设计模式中就已经存在。布局样式,顾名思义就是让元素如何排列的样式,比如左浮动、右浮动、垂直居中、水平居中、Flex 弹性布局、Grid 网格布局。UI 样式,顾名思义就是外观样式,比如颜色、字体、字体大小、圆角(border-radius)、阴影等主要是颜色。

CSS 变量的封装、继承、多态的优势

CSS 变量的封装

我们可以通过在:root 伪类上设置自定义属性也就是 CSS 变量,然后可以在整个页面中需要的地方进行使用,这个可以看作是根据 CSS 变量的特性对 CSS 变量全局作用域的设置,同时这个操作也可以看做是对 UI 样式基础类(父类)封装

CSS 变量的继承

然后在具体的组件中进行继承。比如普通按钮组件的 UI 样式的 CSS 变量如下:

在el-button 中设置的 CSS 变量只能在设置了.el-button 的 HTML 元素上使用,这个可以看作是根据 CSS 变量的特性对 CSS 变量的局部作用域的设置。

CSS 中的多态

在传统编程中,多态是指不同的子类在继承父类后分别都重写覆盖了父类的方法,即父类同一个方法,在继承的子类中表现出不同的形式

同一组件不同的状态有不同的样式实现,这就是 CSS 中的多态。比如 primary 类型的按钮组件的 UI 样式的 CSS 变量设置如下:

这样在 CSS 书写的时候合理设置前后顺序,再根据 CSS 的生效规则,就可以做到.el-button--primary 中的 CSS 变量将覆盖.el-button 中的 CSS 变量,从而实现多态

其实上述的 CSS 使用面向对象的思想:封装继承多态,在传统 CSS 设计模式中也不同的实现,我们目前使用 CSS 变量好像还没体现出其的优势。

使用 CSS 变量的优势

比如 --el-color-primary 属性是在:root 下定义的,然后在 el-button--primary 中通过继承又重新定义出了属于按钮组件新的属性:--el-button-bg-color: var(--el-color-primary),--el-button-border-color: var(--el-color-primary),这样一旦修改--el-color-primary 属性的值,那么--el-button-bg-color 和-el-button-border-color 都将受到影响。因为 --el-color-primary 是定义在:root 下的,是全局作用域的 CSS 变量。如果我们只想修改其中一个组件的 UI 样式,那么我们可以只针对该组件的中定义的 CSS 变量的值进行修改。

一组默认的 primary 按钮组件及复选框的样式如下:

根据上面的分析我们去更改全局的 --el-color-primary 属性。

:root {
  --el-color-primary: green;
}
复制代码

更改之后的样式:

我们可以看到两个 primary 按钮组件背景式都变成了绿色了,而且所有其他组件如果也使用了 --el-color-primary 变量,颜色都将会变成绿色,例如上图中的复选框组件。如果我们只想 Button 组件中的 primary 类型的组件的话,我们根据上文可以知道在 el-button--primary 中通过继承又重新定义出了属于按钮组件新的属性:--el-button-bg-color: var(--el-color-primary)。这样我就可以只修改 --el-button-bg-color 即可。由于作用域的问题,在 class 中定义的 CSS 变量优先级要比在 :root 中的要高,所以修改 --el-button-bg-color 的值,我们需要在 class 中进行修改。

.button-bg-color {
  --el-button-bg-color: green;
}
复制代码

然后在模板中要进行修改的 Button 组件进行添加相应的 class

    Primary1
    Primary2
复制代码

这样就可以达到只对具体某一个组件的样式进行深度定制了。

那么传统修改组件的默认样式,我们可能会像以下方式:

.transfer-content{
    .el-form-item__content{
        .el-input{
            .el-input__inner{
                border-color: #DCDFE6;
            }
        }
        .el-form-item__error{
            width: 200px;
        }
    }
}
复制代码

为了少写 class 需要像套娃一样。

更方便地共享主题色

比如我现在的项目中引用了 Element Plus,那么我也可以在我的项目中通过 CSS 变量使用 Element Plus 中的样式设置。比如说我们知道 primary 类型的组件的主题色是使用的 CSS 变量是 --el-color-primary,那么我们也可以在我们的项目中使用这个变量。

Primary
稀土掘金,一个帮助开发者成长的社区
复制代码

在本地项目中使用 Element Plus 组件库中的变量 --el-color-primary, CSS 样式如下:

.juejin{
  color: var(--el-color-primary)
}
复制代码

渲染结果:

我们发现居然可以轻松使用 Element Plus 组件库中的变量,这样一来我们构建本地项目的 CSS 架构可以和 Element Plus 更加丝滑结合。如果小伙伴使用过 Vue2 的组件库 Element ui (也就是 Element Plus 的 Vue2 版本),实现这个功能的话,得有多繁琐,而且如果更换不同主题色的时候,目前 Element Plus 中使用 CSS 变量,我们只需要更改 CSS 变量的值,即可。

更好地动态更改主题色

以下是花裤衩大佬在他的开源项目中实现的 Vue2 版本的 element ui (也就是 Element Plus 的 Vue2 版本)的动态更改主题色的功能。

它的实现原理,花裤衩大佬在其文档中也有进行说明:“element-ui 2.0 版本之后所有的样式都是基于 SCSS 编写的,所有的颜色都是基于几个基础颜色变量来设置的,所以就不难实现动态换肤了,只要找到那几个颜色变量修改它就可以了。 首先我们需要拿到通过 package.json 拿到 element-ui 的版本号,根据该版本号去请求相应的样式。拿到样式之后将样色,通过正则匹配和替换,将颜色变量替换成你需要的,之后动态添加 style 标签来覆盖原有的 css 样式。”

代码地址:@/components/ThemePicker。

那么在 Element Plus 使用了 CSS 变量之后,进行动态换肤则变得简单很多了。我们只需要修改相应的 CSS 变量值即可。

我们可以通过 useCssVar | VueUse 来控制 CSS 变量的更改和读取。Element Plus 中的主要颜色的 CSS 变量是 --el-color-primary,所以我们只需要控制和更改这个 CSS 变量的值即可。

import { ref } from "vue";
import { useCssVar } from '@vueuse/core'
const color = useCssVar('--el-color-primary', null)
const checked = ref(true)
const value = ref(true)
复制代码

测试模板:

请选择主题色



点击关注
复制代码

渲染结果:

我们可以看到 Element Plus 使用 CSS 变量之后,使用社区的 Vue3 Hooks 函数 useCssVar | VueUse 动态更改主题色是非常简单的了。

useCssVar 的原理

useCssVar 的原理其实也很简单,就是对 CSS 变量读写的 DOM 操作方法的封装。

获取及设置 css 变量的 DOM 方法。

获取全局 CSS 变量

const el = window.document.documentElement

// 获取 css 变量
const value = getComputedStyle(el).getPropertyValue(`--el-color-primary`)
复制代码

获取元素中的 CSS 变量

const el = document.getElementById('xxx')

// 获取 css 变量
const value = getComputedStyle(el).getPropertyValue(`--el-color-primary`)
复制代码

设置 CSS 变量

// 设置 css 变量
el.style.setProperty('--el-color-primary', 'red')
复制代码

那么在 useCssVar 中则不再通过 DOM 方法(document.getElementById)获取元素,而且是事先通过 Vue 中的模板引用 ref 来获取元素。

useCssVar 函数如下:

import { ref, computed, watch } from "vue"

const defaultWindow = window
function useCssVar(
  prop,
  target,
  { window = defaultWindow, initialValue = '' } = {},
) {
  const variable = ref(initialValue)
  const elRef = computed(() => target || window?.document?.documentElement)
  // 获取 CSS 变量值
  watch(
    [elRef, () => prop],
    ([el, prop]) => {
      if (el && window) {
        const value = window.getComputedStyle(el).getPropertyValue(prop)?.trim()
        variable.value = value || initialValue
      }
    },
    { immediate: true },
  )
  // 设置 CSS 变量值
  watch(
    variable,
    (val) => {
      if (elRef.value?.style)
        elRef.value.style.setProperty(prop, val)
    },
  )

  return variable
}
复制代码

那么到这里动态修改组件主题色,即便加上 useCssVar 函数的实现逻辑,也比原来 elmenent ui 中没有使用 CSS 变量的实现要简单得多。

我们现在了解 Element Plus CSS 的一些实现思想和具体的项目应用例子之后,我们接下来就进行深入了解具体的实现逻辑。

SCSS 和 CSS变量的架构实践

我们根据上文已经知道 Element Plus 的 CSS 架构核心思想就是,首先将那些公共的 UI 样式进行提取封装成公共 CSS 变量,相当于基础类,然后每个组件又基于公共的 CSS 变量进行继承封装属于每个组件独立的 CSS 变量,相当于子类

如何封装公共 CSS 变量

所以首先我们需要封装公共的 CSS 变量,我们知道简单的写法就如下:

:root {
  --el-color-primary: green;
}
复制代码

但这样一来便直接写死了,后续不好拓展,比如 CSS 变量的前缀 el,我们知道是可以动态修改的,如果已经写死了的话,则后期拓展性则直接堵死了。所以我们借助 SCSS 的编程能力,通过一个 SCSS 函数来生成 CSS 变量,然后在 SCSS 函数里面则可以做相关的业务逻辑的动态设置了。

在 Element Plus 中是通过设置一个 set-css-var-value 的 SCSS 函数来设置 CSS 变量的。设置 CSS 变量的文件是在 packages/theme-chalk/src/var.scss。

:root {
  @include set-css-var-value('color-white', '#fff');
}
复制代码

像这类设置 CSS 变量的 SCSS 函数在 packages/theme-chalk/src/mixins/_var.scss 文件中进行设置。

@mixin set-css-var-value($name, $value) {
  #{joinVarName($name)}: #{$value};
}
复制代码

set-css-var-value 函数则是通过 joinVarName 函数根据参数 $name 拼接 CSS 变量名。joinVarName 函数则是在 packages/theme-chalk/src/mixins/function.scss 文件中定义的。

@function joinVarName($list) {
  $name: '--' + config.$namespace;
  @each $item in $list {
    @if $item != '' {
      $name: $name + '-' + $item;
    }
  }
  @return $name;
}
复制代码

参数 $list 是一个数组,例如 joinVarName(('button', 'text-color')) 则可以生成 --el-button-text-color。这里我们可以看到前缀 el 是通过 SCSS 配置变量 config.$namespace,那么在开发项目中则可以修改 config.$namespace 变量更改 CSS 变量的前缀。

在项目的开发阶段我们可以通过以下方式更改 Element Plus 的 CSS 变量前缀。

@forward "element-plus/theme-chalk/src/mixins/config.scss" with (
  $namespace: "ep"
);
复制代码

样式目录文件架构

到此我们有必要对 Element Plus 中的样式目录文件的架构组成做一个配置解析,以便我们从一个更目录架构的维度去了解 Element Plus 中的样式架构思想。

├── packages
│   ├── theme-chalk
│   │   ├── src
│   │   │   ├── common
│   │   │   │   └── var.scss // SCSS 变量配置文件
│   │   │   ├── mixins
│   │   │   │   ├── _var.scss // CSS 变量相关的 SCSS 自定义函数
│   │   │   │   ├── config.scss // 配置文件
│   │   │   │   ├── function.scss // 全局的 SCSS 自定义函数
│   │   │   │   └── mixins.scss  // BEM 相关函数
│   │   │   ├── base.scss // 基础必须要引用的文件
│   │   │   ├── index.scss // 全量导入组件库的 CSS 入口文件
│   │   │   ├── var.scss   // CSS 变量配置文件
│   │   │   ├── reset.scss // 默认样式重置配置文件
│   │   │   ├── button.scss // 按需导入组件的 CSS 文件
│   │   └── package.json // 主题包的 npm 配置文件
复制代码

从文件目录架构和结合上文进行理解,我们需要做的就是通过 theme-chalk/src/common/var.scss 中的 SCSS 变量配置在 theme-chalk/src/var.scss 中进行封装生成全局的 CSS 变量,在这个过程中使用到的有关生成 CSS 变量的 SCSS 自定义函数则在 theme-chalk/src/mixins/_var.scss 文件中进行设置。

因为要实现按需导入组件,所以各个组件单独一个 CSS 文件。例如 Button 组件的 CSS 文件对应就是 button.scss 文件。在编写组件 CSS 样式中需要使用到的 BEM 的 SCSS 自定义函数则在 theme-chalk/src/mixins/mixins.scss 中,其他的 SCSS 自定义函数则在 theme-chalk/src/mixins/function.scss 中。

base.scss 文件则是引用那些必须的文件,比如 var.scss 文件是必须,这类文件则放在 base.scss 文件中。在全量导入组件库的 CSS 入口文件 index.scss 中最先引入的便是 base.scss 文件。将来组件的自己的按需导入的 CSS 文件,例如 packages/components/button/style/index.ts 也就是 Button 组件的按需导入的 CSS 文件,其中也是需要最先导入 base.scss 文件,再进行导入 button.scss 文件。

SCSS 样式变量

我们希望在设置生成 CSS 变量的时候是根据 SCSS 的变量动态生成的,这样我们用户在使用的时候,如果需要进行大规模替换样式,可以通过直接覆盖 Element Plus 中的 SCSS 变量来实现。

:root {
  @include set-css-var-value('color-white', $color-white);
}
复制代码

上述 $color-white 变量设置在 packages/theme-chalk/src/common/var.scss 文件中。

我们知道 Element Plus 中有 primary、success、warning、danger、error、info 六种类型颜色。我们需要对这六种类型的颜色设置 SCSS 变量。

$colors: () !default;
$colors: map.deep-merge(
  (
    'white': #ffffff,
    'black': #000000,
    'primary': (
      'base': #409eff,
    ),
    'success': (
      'base': #67c23a,
    ),
    'warning': (
      'base': #e6a23c,
    ),
    'danger': (
      'base': #f56c6c,
    ),
    'error': (
      'base': #f56c6c,
    ),
    'info': (
      'base': #909399,
    ),
  ),
  $colors
);

$color-white: map.get($colors, 'white') !default;
$color-black: map.get($colors, 'black') !default;
$color-primary: map.get($colors, 'primary', 'base') !default;
$color-success: map.get($colors, 'success', 'base') !default;
$color-warning: map.get($colors, 'warning', 'base') !default;
$color-danger: map.get($colors, 'danger', 'base') !default;
$color-error: map.get($colors, 'error', 'base') !default;
$color-info: map.get($colors, 'info', 'base') !default;
复制代码

先设置一个总的 $colors 变量,然后再单独设置每一个具体类型的变量,变量值是继承 $colors 变量,这样将来只要修改 $colors 中的变量值,后续每个继承 $colors 变量的 SCSS 变量都将发生改变。

值得注意的是 $colors: () !default; 中 () 写法是 SCSS 中变量空数组的表示方式,!default; 表示默认变量,将来可以使用 @forward 修改默认变量。那么为什么不直接把相关变量设置写到默认变量中呢?因为将来用户在使用 @forward 修改 SCSS 中的默认变量时,有可能只修改其中部分的选项,并不是全部修改,如果相关变量写在默认变量中,那么使用 @forward 修改默认变量时,就不能修改部分选项了。目前这种通过 map.deep-merge 合并默认变量的选项的方式则可以达到将来只修改部分选项的目的,变得更加灵活。

修改 Element Plus 中的 SCSS 变量:

// styles/element/index.scss
/* 只需要重写你需要的即可 */
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
  $colors: (
    'primary': (
      'base': green,
    ),
  ),
);

// 如果只是按需导入,则可以忽略以下内容。
// 如果你想导入所有样式:
// @use "element-plus/theme-chalk/src/index.scss" as *;
复制代码

这样就只会修改 primary 选项的值。

如何设置组件的多种类型主题

Element Plus 中 Button 组件有 primary、success、warning、danger、error、info 六种类型颜色,而且在不同状态下的都是这六种颜色的相近的颜色。

比如 primary 类型的按钮的默认的背景色是 --el-color-primary,鼠标悬浮的时候则是 --el-color-primary-light-3。

plain 样式的 primary 类型的按钮,即:Primary 的背景色则是 --el-color-primary-light-9。

--el-color-primary 和 --el-color-primary-light-3 都是相近的颜色,此外还有 --el-color-primary-light-7, --el-color-primary-light-8,--el-color-primary-light-9。详细如下图:

这是根据使用场景的定义的颜色粒度。这么多类型这么多不同的颜色,在 Element Plus 中是怎么实现的呢?

首先定义一个 SCSS 函数 set-color-mix-level 拓展 $colors 变量中的颜色。

@mixin set-color-mix-level(
  $type,
  $number,
  $mode: 'light',
  $mix-color: $color-white
) {
  $colors: map.deep-merge(
    (
      $type: (
        '#{$mode}-#{$number}':
          mix(
            $mix-color,
            map.get($colors, $type, 'base'),
            math.percentage(math.p($number, 10))
          ),
      ),
    ),
    $colors
  ) !global; // 将局部变量转换为全局变量
}
复制代码

Sass:RGB颜色函数-Mix() 函数

Mix 函数是将两种颜色根据一定的比例混合在一起,生成另一种颜色。其使用语法如下: mix($color-1,$color-2,$weight); $color-1 和 $color-2 指的是你需要合并的颜色,颜色可以是任何表达式,也可以是颜色变量。$weight为合并的比例(选择权重),默认值为 50%,其取值范围是 0~1 之间。它是每个 RGB 的百分比来衡量,当然透明度也会有一定的权重。默认的比例是 50%,这意味着两个颜色各占一半,如果指定的比例是 25%,这意味着第一个颜色所占比例为 25%,第二个颜色所占比例为75%。

math.percentage 将一个不带单位的数转换成百分比值。

与其他数学运算不同,Sass 中的除法是通过 math.p() 函数完成的。

!global 将局部变量转换为全局变量

生成 light 模式的颜色,就是亮色模式

// types
$types: primary, success, warning, danger, error, info;

@each $type in $types {
  @for $i from 1 through 9 {
    @include set-color-mix-level($type, $i, 'light', $color-white);
  }
}
复制代码

执行上述代码就等于往各类型中添加新的变量颜色,相当于下面的代码的效果。

$colors:
  (
    'primary': (
      'base': #409eff,
      'light-1': 通过mix生成的新颜色,
      'light-2': 通过mix生成的新颜色,
      'light-3': 通过mix生成的新颜色,
      // ...
    ),
    'success': (
      'base': #67c23a,
      'light-1': 通过mix生成的新颜色,
      'light-2': 通过mix生成的新颜色,
      'light-3': 通过mix生成的新颜色,
      // ...
    ),
    'warning': (
      'base': #e6a23c,
      'light-1': 通过mix生成的新颜色,
      'light-2': 通过mix生成的新颜色,
      'light-3': 通过mix生成的新颜色,
      // ...
    ),
    // ...
  )
复制代码

对应的还有浅色模式的颜色生成。

// types
$types: primary, success, warning, danger, error, info;

@each $type in $types {
  @include set-color-mix-level($type, 2, 'dark', $color-black);
}
复制代码

我们可以通过 SCSS 提供的 @debug 函数进行查看生成后的 $colors,即:@debug $colors。然后可以在终端看到以下输出:

(
info: (
"dark-2": #73767a, 
"light-9": #f4f4f5, 
"light-8": #e9e9eb, 
"light-7": #dedfe0, 
"light-6": #d3d4d6, 
"light-5": #c8c9cc, 
"light-4": #bcbec2, 
"light-3": #b1b3b8, 
"light-2": #a6a9ad, 
"light-1": #9b9ea3, 
"base": #909399), 
error: (
"dark-2":#c45656, 
"light-9": #fef0f0, 
"light-8": #fde2e2, 
"light-7": #fcd3d3, 
"light-6": #fbc4c4, 
"light-5": #fab6b6, 
"light-4": #f9a7a7, 
"light-3": #f89898, 
"light-2": #f78989, 
"light-1": #f67b7b, 
"base": #f56c6c), 
danger: (
"dark-2": #c45656, 
"light-9": #fef0f0, 
"light-8": #fde2e2, 
"light-7": #fcd3d3, 
"light-6": #fbc4c4, 
"light-5": #fab6b6, 
"light-4": #f9a7a7, 
"light-3": #f89898, 
"light-2": #f78989, 
"light-1": #f67b7b, 
"base": #f56c6c), 
warning: (
"dark-2": #b88230, 
"light-9": #fdf6ec, 
"light-8": #faecd8,
"light-7": #f8e3c5, 
"light-6": #f5dab1, 
"light-5": #f3d19e, 
"light-4": #f0c78a, 
"light-3": #eebe77, 
"light-2": #ebb563, 
"light-1": #e9ab50,
"base": #e6a23c), 
success: (
"dark-2": #529b2e, 
"light-9": #f0f9eb, 
"light-8": #e1f3d8, 
"light-7": #d1edc4, 
"light-6": #c2e7b0, 
"light-5": #b3e19d, 
"light-4": #a4da89, 
"light-3": #95d475, 
"light-2": #85ce61, 
"light-1": #76c84e, 
"base": #67c23a), 
primary: (
"dark-2": #337ecc, 
"light-9": #ecf5ff, 
"light-8": #d9ecff, 
"light-7": #c6e2ff, 
"light-6": #b3d8ff, 
"light-5": #a0cfff, 
"light-4": #8cc5ff, 
"light-3": #79bbff, 
"light-2": #66b1ff, 
"light-1": #53a8ff, 
"base": #409eff), 
"white": #ffffff, 
"black": #000000
)
复制代码

接下来我们就要去生成以下的全局 CSS 变量了。

这些属于亮色模式的样式(更多可以查看《10. CSS 系统颜色和暗黑模式的关系及意义》),所以我们要另起一个 :root 并且加上 color-scheme: light,告诉浏览器使用亮色模式进行渲染。我们通过循环 primary, success, warning, danger, error, info 再通过自定义 SCSS 函数 set-css-color-type 去生成相关的 CSS 变量。

:root {
  color-scheme: light;

  // --el-color-#{$type}
  // --el-color-#{$type}-light-{$i}
  @each $type in (primary, success, warning, danger, error, info) {
    @include set-css-color-type($colors, $type);
  }
}
复制代码

set-css-color-type 函数。

@mixin set-css-color-type($colors, $type) {
   // 生成 --el-color-#{$type}
  @include set-css-var-value(('color', $type), map.get($colors, $type, 'base'));
   // 生成 --el-color-#{$type}-light-{$i}
  @each $i in (3, 5, 7, 8, 9) {
    @include set-css-var-value(
      ('color', $type, 'light', $i),
      map.get($colors, $type, 'light-#{$i}')
    );
  }
  // 生成 --el-color-#{$type}-dark-2
  @include set-css-var-value(
    ('color', $type, 'dark-2'),
    map.get($colors, $type, 'dark-2')
  );
}
复制代码

上述主要是讲解了生成 primary, success, warning, danger, error, info 相关的全局 CSS 变量。

以分组生成 CSS 变量

在 Element Plus 的 SCSS 架构中还有一个很重要的思想,就是以分组的模式生成不通过分组的 CSS 变量。比如文本颜色,会有很多种颜色,那么文本颜色就为一个分组,还有背景颜色,边框颜色,还有我们每个组件的需要用到的 CSS 变量,也是以一个组件为分组生成一组 CSS 变量。

背景颜色

$bg-color: () !default;
$bg-color: map.merge(
  (
    '': #ffffff,
    'page': #f2f3f5,
    'overlay': #ffffff,
  ),
  $bg-color
);
复制代码

文本颜色

$text-color: () !default;
$text-color: map.merge(
  (
    'primary': #303133,
    'regular': #606266,
    'secondary': #909399,
    'placeholder': #a8abb2,
    'disabled': #c0c4cc,
  ),
  $text-color
);
复制代码

边框颜色

$border-color: () !default;
$border-color: map.merge(
  (
    '': #dcdfe6,
    'light': #e4e7ed,
    'lighter': #ebeef5,
    'extra-light': #f2f6fc,
    'dark': #d4d7de,
    'darker': #cdd0d6,
  ),
  $border-color
);
复制代码

这些分组都是通过定义一个 SCSS 函数 set-component-css-var 来生成。

:root {
  color-scheme: light;
  @include set-component-css-var('bg-color', $bg-color);
  // --el-text-color-#{$type}
  @include set-component-css-var('text-color', $text-color);
  // --el-border-color-#{$type}
  @include set-component-css-var('border-color', $border-color);
}
复制代码

SCSS 函数 set-component-css-var。

@mixin set-component-css-var($name, $variables) {
  @each $attribute, $value in $variables {
    @if $attribute == 'default' {
      #{getCssVarName($name)}: #{$value};
    } @else {
      #{getCssVarName($name, $attribute)}: #{$value};
    }
  }
}
复制代码

到目前为止,我们上文讲解了那么多,都是为了我们开篇所讲的核心思想,将那些公共的 UI 样式进行提取封装成公共 CSS 变量。

Button 组件的样式实现

我们上文说到每个组件会基于公共的 CSS 变量进行继承封装属于每个组件独立的 CSS 变量,那么我们以 Button 组件为例进行说明,因为 Button 组件的业务代码和 HTML 结构我们已经在前面的章节《9. 组件开发中 Vue3 相关知识的应用与解析及 Button 组件的实现》实现了,现在实现样式部分。

首先我们要实现组件自身基于公共 CSS 变量进行封装的 CSS 变量。通过上面我们知道为了方便拓展和维护,我们将 CSS 变量值的设置在 SCSS 的变量文件中。

先在 packages heme-chalksrccommonvar.scss 中设置 Button 相关变量。

$button: () !default;
$button: map.merge(
  (
    'font-weight': getCssVar('font-weight-primary'),
    'border-color': getCssVar('border-color'),
    'bg-color': getCssVar('fill-color', 'blank'),
    'text-color': getCssVar('text-color', 'regular'),
    // ...
  ),
  $button
);
复制代码

这里我们看到 Button 组件相关 UI 样式的 SCSS 变量的值是 getCssVar 函数获取的全局的 CSS 变量,这样将来生成新的 CSS 变量的值就是继承于全局的 CSS 变量。

然后在 packages/theme-chalk/src/button.scss 文件中设置 Button 组件相关的 CSS 变量。

@use 'sass:map';
@use 'common/var' as *;
@use 'mixins/mixins' as *;
@use 'mixins/var' as *;

@include b(button) {
  @include set-component-css-var('button', $button);
}
复制代码

@mixin b() 方法是我们前面文章中说到生成 BEM 的 CSS 命名规范函数,详细了解可以看这篇文章 《6. CSS 架构模式之 BEM 在组件库中的实践》。set-component-css-var 方法是我们上文中说过了的,它会根据设置好的一组 SCSS 变量进行生成对应的 CSS 变量。

经过上面的 SCSS 代码就会生成默认的 Button 组件的 CSS 变量。

正如上文所说 Button 组件生成的 CSS 变量的值是全局的 CSS 变量,当然有些特别的变量不是。

同样其他组件的 CSS 变量也是如此生成。比如 checkbox 组件。

@include b(checkbox) {
  @include set-component-css-var('checkbox', $checkbox);
}
复制代码

生成组件 CSS 变量之后,我们就可以去设置组件的样式了。

@include b(button) {
  display: inline-flex;
  justify-content: center;
  align-items: center;

  line-height: 1;
  height: map.get($input-height, 'default');
  white-space: nowrap;
  cursor: pointer;
  color: getCssVar('button', 'text-color');
  text-align: center;
  box-sizing: border-box;
  outline: none;
  transition: 0.1s;
  font-weight: getCssVar('button', 'font-weight');
  user-select: none;
  vertical-align: middle;
  -webkit-appearance: none;
  background-color: getCssVar('button', 'bg-color');
  border: getCssVar('border');
  border-color: getCssVar('button', 'border-color');
}
复制代码

生成的最终的页面 CSS 结果如下:

通过结果我们可以清晰看出组件的相关 UI 样式是获取组件本身的 CSS 变量。主要是通过 getCssVar 这个函数来实现的。那么我们来看看 getCssVar 函数的实现。

@function getCssVar($args...) {
  @return var(#{joinVarName($args)});
}

@function joinVarName($list) {
  $name: '--' + config.$namespace;
  @each $item in $list {
    @if $item != '' {
      $name: $name + '-' + $item;
    }
  }
  @return $name;
}
复制代码

其实很简单就是通过传进来的参数进行拼接成 CSS 变量的名称,在 Button 组件中设置 getCssVar('button', 'bg-color') 将会生成 --el-button-bg-color,那么获取的就是组件的 CSS 变量,如果是 getCssVar('bg-color') 将会生成 --el-bg-color,那么获取的就是全局的 CSS 变量。在 packages heme-chalksrccommonvar.scss 中设置 Button 相关变量时,通过 getCssVar 获取的 CSS 变量也是这个原理。

Button 组件和其他组件的样式有一个最大的不同就是,Button 组件默认是有 primary, success, warning, danger, info,加上默认的主题色,一共有六种主题色,而我们上面实现的是默认的主题色,也是所有其他组件的默认实现方式。

实现的原理也很简单就是上文所说的多态,具体就是根据不同的 classname 来进行实现不同的 UI 样式的 CSS 变量,对应的分别是 el-button--primary、el-button--success、el-button--warning、el-button--danger、el-button--info。在 BEM 规范中 primary, success, warning, danger, info 属于 Button 组件的不同修改器,要生成修改器的 classname 则需要通过 @mixin m()。

具体实现如下:

@include b(button) {
  @each $type in (primary, success, warning, danger, info) {
    @include m($type) {
      @include button-variant($type);
    }
  }
}
复制代码

再定义一个 button-variant 函数进行根据不同类型生成不同的修改器的 CSS 变量。又因为这个函数是 Button 组件特有的,所以新建一个专门属于 Button 组件的 SCSS @mixin 函数文件。也就是 packages/theme-chalk/src/mixins/_button.scss。

@mixin button-variant($type) {
  $button-color-types: (
    '': (
      'text-color': (
        'color',
        'white',
      ),
      'bg-color': (
        'color',
        $type,
      ),
      'border-color': (
        'color',
        $type,
      ),
     // ...
    ),
  );

  @each $type, $typeMap in $button-color-types {
    @each $typeColor, $list in $typeMap {
      @include css-var-from-global(('button', $type, $typeColor), $list);
    }
  }
}
复制代码

css-var-from-global 函数的实现

@mixin css-var-from-global($var, $gVar) {
  $varName: joinVarName($var);
  $gVarName: joinVarName($gVar);
  #{$varName}: var(#{$gVarName});
}
复制代码

可以看到原理也很简单,就是根据第一个参数生成对应 Button 组件的 CSS 变量名称,根据第二个参数生成对应全局的 CSS 变量名称,然后通过原生 CSS 函数 var 读取全局 CSS 变量。

经过上述设置,我们在 play 项目中进行测试我们编写的 Button 组件的样式

Default
Primary
Success
Info
Warning
Danger
复制代码

我们启动 play 项目

最终渲染结果如下:

至此我们讲解了 Element PLus 中的 CSS 架构思想,以及通过实现 Button 组件的样式来实践 Element PLus 中的 CSS 架构思想。

暗黑模式

通过 CSS 变量,Element Plus 默认提供了“白天模式”和“夜间模式” (又或着叫:暗黑模式) 两种皮肤。我们在上文中其中主要实现的都是浅色模式的颜色,因为默认就是浅色模式。暗黑模式的实现其实跟 Button 组件中的不同类型的主题的实现是本质原理是一样的,就是根据 CSS 变量的优先级进行覆盖。只要设置暗黑模式中的 CSS 变量比浅色模式中的 CSS 变量优先级更高,就可以达到当页面切换到暗黑模式的时候,暗黑模式中的 CSS 变量就会覆盖浅色模式的 CSS 变量。

在项目中如何使用,可以查看官方文档 Element Plus 终于支持了黑暗模式。

总结

Element Plus 的 CSS 架构并没有脱离传统的 CSS 设计模式,主要还是 OOCSS,也就是还是面向对象的思想,即:封装、继承、多态,并且 CSS 变量的加入使用,使得多态这一特性的实现更加丝滑。

整个 CSS 架构核心思想就是,首先将那些公共的样式进行提取封装成公共 CSS 变量,相当于基础类,然后每个组件又基于公共的 CSS 变量进行继承封装属于每个组件独立的 CSS 变量,相当于子类。这样一旦修改基础类的 CSS 变量,所有继承基础类的组件的样式都会发生改变,这个就是使用 CSS 变量进行封装继承的好处,但如果只想对其中某一个组件的样式做深度的定制,则可以只修改该组件的 CSS 变量,这样就实现了多态

值得注意的是CSS 变量可以在运行时进行更改值,这也使得整个组件库的样式更改变化有了非常大的灵活性以及方便性,这意味着你可以动态地改变组件内的个别变量,就可以更好地自定义组件样式,而不需要修改 SCSS 文件重新编译一次。

我们理解了 Element Plus 的 CSS 架构的这些核心思想之后,我们再去研究和阅读 Element Plus 的 CSS 源码则会有一种豁然开朗的感觉,因为它所做的一切都是基于上述的核心思想,所谓理论指导实践,当然也可以在实践中总结理论。当然除了上述思想外,Element Plus 的 CSS 架构还要实现一些组件特有的业务需求,比如按需加载组件的样式如何处理,这些可以在我们详细了解 Element Plus 的 CSS 架构之后再进行具体学习研究。

展开阅读全文

页面更新:2024-04-29

标签:架构   组件   模式   变量   样式   函数   颜色   思想   专栏   代码   文件

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top