一、前言
设计一个组件库的样式最重要的就是一点就是样式的高复用性和低耦合度。在一个项目中每个组件会用大量的公共样式,为了确保组件在不同的场景、布局中的样式保持统一,减少样式干扰的可能性。 组件样式的命名也是非常需要统一管理的,一个优秀的命名能够增加样式的可读性,组件库中的组件可能会嵌套使用,所以封装公共样式和统一命名规范由为重要,它也直接决定了组件的易用性和扩展性。
这篇文章我将详细的向你介绍 Element Plus 中样式是如何封装设计的。
二、组件样式设计思路
Element Plus 中使用了 BEM 来给组件样式的类名来命名,整个组件库中所共有的 sass变量也是大致按照这个来命名的,统一命名规则,可以使整个组件库的样式架构复用性和拓展性变得更强。正是因为采用了 BEM 命名规则命名类名,所以在封装样式类名也需要遵循这个规则来封装。
Element Plus 在 packages\components目录下创建一个 base目录来导入 packages\theme-chalk 目录下的公共样式( root )和一些基本公共样式,这个目录下packages\components\base\style\css.ts 的文件可以导入打包好的全局 css 样式,加快渲染速度。采用这种目录结果管理样式是为了分支管理,尽可能的降低各个项目之间的耦合度。而packages/components 目录下的组件可以引用 components/base 目录下的公共样式。
每个组件下都可以创建 style文件夹来导入公共样式和组件自身的样式,组件还需要将打包好的 css 样式在 css.ts 文件下导入,这样可以提前加载出 css 样式,优化了性能。
三、组件库封装样式的具体实现
一)、类名 BEM 实现
我们再 packages\hooks 目录下创建一个 use-namespace 文件,在这个目录下来完成组件的命名规范函数封装。
const _bem = (
namespace: string,
block: string,
blockSuffix: string,
element: string,
modifier: string
) => {
let cls = `${namespace}-${block}`
if (blockSuffix) {
cls += `-${blockSuffix}`
}
if (element) {
cls += `__${element}`
}
if (modifier) {
cls += `--${modifier}`
}
return cls
}
namespace: 命名空间,一般用于区分不同的模块。block: BEM 中的块(Block),表示组件的名称。blockSuffix: 块后缀(Block Suffix),通常用于进一步细化块的分类。element: BEM 中的元素(Element),表示块内部的某个具体部分。modifier: BEM 中的修饰符(Modifier),用于描述块或元素的状态或属性。
根据这命名函数的条件分支,我们可以再细分一些处理函数,可以使用更加方便再一些特定的情况下。我们将这三个条件进行组合,生成六个处理函数,因为有些组件需要设计某些字段是否存在,存在就添加上对应的样式,我们可以通过 is 来实现。 这些就已经包括了组件中可能会用到的 class 类名称。
export const useNamespace = (
block: string,
namespaceOverrides?: Ref<string | undefined>
) => {
const namespace = ref('fz')
/**
* 生成 BEM 格式的块级类名
* 示例:
* 如果 block 为 'button',调用 b() 将返回 'fz-button'
*/
const b = (blockSuffix = '') =>
_bem(namespace.value, block, blockSuffix, '', '')
/**
* 生成 BEM 格式的元素类名
* 示例:
* 如果 block 为 'button',调用 e('icon') 将返回 'fz-button__icon'
*/
const e = (element?: string) =>
element ? _bem(namespace.value, block, '', element, '') : ''
/**
* 生成 BEM 格式的修饰符类名
* 示例:
* 如果 block 为 'button',调用 m('disabled') 将返回 'fz-button--disabled'
*/
const m = (modifier?: string) =>
modifier ? _bem(namespace.value, block, '', '', modifier) : ''
/**
* 生成 BEM 格式的块级后缀和元素类名
* 示例:
* 如果 block 为 'button',调用 be('primary', 'icon') 将返回 'fz-button-primary__icon'
*/
const be = (blockSuffix?: string, element?: string) =>
blockSuffix && element
? _bem(namespace.value, block, blockSuffix, element, '')
: ''
/**
* 生成 BEM 格式的元素和修饰符类名
* 示例:
* 如果 block 为 'button',调用 em('icon', 'active') 将返回 'fz-button__icon--active'
*/
const em = (element?: string, modifier?: string) =>
element && modifier
? _bem(namespace.value, block, '', element, modifier)
: ''
/**
* 生成 BEM 格式的块级后缀和修饰符类名
* 示例:
* 如果 block 为 'button',调用 bm('primary', 'disabled') 将返回 'fz-button-primary--disabled'
*/
const bm = (blockSuffix?: string, modifier?: string) =>
blockSuffix && modifier
? _bem(namespace.value, block, blockSuffix, '', modifier)
: ''
/**
* 生成 BEM 格式的完整类名
* 示例:
* 如果 block 为 'button',调用 bem('primary', 'icon', 'active') 将返回 'fz-button-primary__icon--active'
*/
const bem = (blockSuffix?: string, element?: string, modifier?: string) =>
blockSuffix && element && modifier
? _bem(namespace.value, block, blockSuffix, element, modifier)
: ''
/**
* 生成状态类名
* 示例:
* 调用 is('active', true) 将返回 'is-active'
* 调用 is('active', false) 将返回空字符串
*/
const is: {
(name: string, state: boolean | undefined): string
(name: string): string
} = (name: string, ...args: [boolean | undefined] | []) => {
const state = args.length >= 1 ? args[0]! : true
return name && state ? `${statePrefix}${name}` : ''
}
namespace,
b,
e,
m,
be,
em,
bm,
bem,
is,
}
}
二)、sass 样式封装实现
因为Element Plus 组件库中用到的 Sass 是比较高级的,如果之前没有了解过 Sass 需要恶补一下相关内容。这里就不过多赘述了,推荐 blog.csdn.net/weixin_4464… 这篇博客文章,这里已经详细的介绍了组件库中会使用上的知识点了。
1. Sass mixins 实现 BEM命名
根据类名的 BEM 的命名规则,Element Plus 设计 Sass 的也需要遵守这个规则来实现。我们采用 Sass 中的 mixins 混合指令来实现。在 packages\theme-chalk\src 目录下创建 mixins 目录来管理整个组件库中会使用到的 mixins。
//BEM
// The `b` mixin defines the base block name for a component following the BEM naming convention.
// Example usage:
// @include b('button') {
// color: blue;
// }
// .fz-button {
// color: blue;
// }
@mixin b($block) {
$B: $namespace + $common-separator + $block !global;
.#{$B} {
@content;
}
}
// The `when` mixin is used to define a state for a component, such as 'is-active' or 'is-disabled'.
// Example usage:
// @include b('button') {
// ...
// @include when('disabled') {
// opacity: 0.5;
// }
// }
// .fz-button {
// ...
// }
// .fz-button.is-disabled {
// opacity: 0.5;
// }
@mixin when($state) {
@at-root {
&.#{$stateSuffix + $state} {
@content;
}
}
}
// The `e` mixin defines elements of a block following the BEM convention.
// Example usage:
// @include b('card') {
// ...
// @include e('header') {
// font-size: 16px;
// }
// }
// .fz-card {
// ...
// }
// .fz-card__header {
// font-size: 16px;
// }
@mixin e($element) {
$E: $element !global;
$selector: &;
$currentSelector: '';
@each $unit in $element {
$currentSelector: #{$currentSelector +
$selector +
$fz-separator +
$unit +
','};
}
@if hitAllSpecialNestRule($selector) {
@at-root {
#{$selector} {
#{$currentSelector} {
@content;
}
}
}
} @else {
@at-root {
#{$currentSelector} {
@content;
}
}
}
}
// The `m` mixin defines modifiers of a block or element following the BEM convention.
// Example usage:
// @include b('button') {
// ...
// @include m('primary') {
// background-color: blue;
// }
// }
// .fz-button {
// ...
// }
// .fz-button--primary {
// background-color: blue;
// }
@mixin m($modifier) {
$selector: &;
$currentSelector: '';
@each $unit in $modifier {
$currentSelector: #{$currentSelector +
$selector +
$modifier-separator +
$unit +
','};
}
@at-root {
#{$currentSelector} {
@content;
}
}
}
每个 mixin 的用法我都加上了注释,尤其注意 @mixin e() 中的 $selector:& ,代表了父级全部选择器。
.button {
.is-disabled {
@include e(("icon", "label")) {
color: gray;
}
}
}
在这段代码中,& 的值是:.button.is-disabled,它的输出结果是
.button.is-disabled {
.button__icon,
.button__label {
color: gray;
}
}
在使用 @mixins e() 时,我们可能会在某个父级选择器中使用,所以我们父级选择器会可能会有修饰符 -- 、 .is- 或者伪类的 : 的情况下。 所以我们需要针对这种情况做出额外处理,我们希望保持原来的选择器嵌套结构。
我们将这个处理逻辑写在 packages\theme-chalk\src\mixins\function.scss目录下,这个目录用来管理 Sass中使用到的 function 。
// 将选择器转换为字符串并去掉第一个字符和最后一个字符
@function selectorToString($selector) {
// 将选择器转换为字符串形式
$selector: inspect($selector);
// 去掉第一个字符和最后一个字符
$selector: str-slice($selector, 2, -2);
// 返回处理后的选择器字符串
@return $selector;
}
// 检查选择器是否包含修饰符 (modifier)
// 示例输入: '.button--primary'
// 示例输出: true
@function containsModifier($selector) {
// 将选择器转换为字符串
$selector: selectorToString($selector);
// 检查是否包含修饰符分隔符(通常是两个短横线 --)
@if str-index($selector, config.$modifier-separator) {
@return true;
} @else {
@return false;
}
}
// 检查选择器是否包含特定状态标记
// 示例输入: '.button.is-disabled'
// 示例输出: true
@function containWhenFlag($selector) {
// 将选择器转换为字符串
$selector: selectorToString($selector);
// 检查是否包含状态前缀(通常是 .is-)
@if str-index($selector, '.' + config.$state-prefix) {
@return true;
} @else {
@return false;
}
}
// 检查选择器是否包含伪类
// 示例输入: '.button:hover'
// 示例输出: true
@function containPseudoClass($selector) {
// 将选择器转换为字符串
$selector: selectorToString($selector);
// 检查是否包含伪类标记(通常是冒号 :)
@if str-index($selector, ':') {
@return true;
} @else {
@return false;
}
}
// 综合判断选择器是否符合以下任一规则:
// 1. 包含修饰符
// 2. 包含特定状态标记
// 3. 包含伪类
// 示例输入: '.button--primary.is-disabled:hover'
// 示例输出: true
@function hitAllSpecialNestRule($selector) {
@return containsModifier($selector) or containWhenFlag($selector) or
containPseudoClass($selector);
}
2. Sass 封装组件库的样式变量名和变量值
组件库中设计变量名也是需要统一的,一般大部分组件用到的样式都是一致的,通过 var() 来使用变量,而大量的变量也需要进行封装。
// join var name
// 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;
}
// getCssVarName('button', 'text-color') => '--el-button-text-color'
@function getCssVarName($args...) {
@return joinVarName($args);
}
// getCssVar('button', 'text-color') => var(--el-button-text-color)
@function getCssVar($args...) {
@return var(#{joinVarName($args)});
}
// getCssVarWithDefault(('button', 'text-color'), red) => var(--el-button-text-color, red)
@function getCssVarWithDefault($args, $default) {
@return var(#{joinVarName($args)}, #{$default});
有了变量名,我们还需要声明全局的变量值,为了统一组件样式,我们将它放在 packages\theme-chalk\src\common\var.scss进行管理,组件的单独样式,组件库的全局样式我们都可以写在这个文件夹下。
我们先声明一些基本公共样式,颜色、填充色、字体大小、组件尺寸、边框、字体样式、背景色 等等。
@use 'sass:math';
@use 'sass:map';
@use '../mixins/function.scss' as *;
// types
$types: primary, success, warning, danger, info;
//Color
$colors: () !default;
$colors: map.deep-merge(
(
'white': #ffffff,
'black': #000000,
'primary': (
'base': '#409eff',
),
'success': (
'base': #67c23a,
),
'warning': (
'base': #e6a23c,
),
'danger': (
'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-info: map.get($colors, 'info', 'base') !default;
//mix colors white/black to generate with light/dark colors
@mixin set-color-mix-level(
$type,
$number,
$mode: 'light',
$mix-color: $color-white
) {
$colors: map.deep-merge(
(
$types: (
'#{$mode}-#{$number}': (
mix(
$mix-color,
map.get($colors, $type, 'base'),
math.percentage(math.div($number, 10))
),
),
),
),
$colors
) !global;
}
// $colors: (
// primary: (
// light-1: '',
// ),
// );
// --fz-color-primary-light-i
// 10% 53a8ff
// 20% 66b1ff
// 30% 79bbff
// 40% 8cc5ff
// 50% a0cfff
// 60% b3d8ff
// 70% c6e2ff
// 80% d9ecff
// 90% ecf5ff
@each $type in $types {
@for $i from 1 through 10 {
@include set-color-mix-level($type, $i, 'light', $color-white);
}
}
// --el-color-primary-dark-2
@each $type in $types {
@include set-color-mix-level($type, 2, 'dark', $color-black);
}
$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
);
$fill-color: () !default;
$fill-color: map.merge(
(
'': #f0f2f5,
'light': #f5f7fa,
'lighter': #fafafa,
'extra-light': #fafcff,
'dark': #ebedf0,
'darker': #e6e8eb,
'blank': #ffffff,
),
$fill-color
);
// Background
$bg-color: () !default;
$bg-color: map.merge(
(
'': #ffffff,
'page': #f2f3f5,
'overlay': #ffffff,
),
$bg-color
);
// Border
$border-width: 1px !default;
$border-style: solid !default;
$border-color-hover: getCssVar('text-color', 'disabled') !default;
// border radius
$border-radius: () !default;
$border-radius: map.merge(
(
'base': 4px,
'small': 2px,
'round': 20px,
'circle': 100%,
),
$border-radius
);
// Typography
$font-family: () !default;
$font-family: map.merge(
(
// default family
'':
"'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif"
),
$font-family
);
$font-size: () !default;
$font-size: map.merge(
(
'extra-large': 20px,
'large': 18px,
'medium': 16px,
'base': 14px,
'small': 13px,
'extra-small': 12px,
),
$font-size
);
$common-component-size: () !default;
$common-component-size: map.merge(
(
'large': 40px,
'default': 32px,
'small': 24px,
),
$common-component-size
);
// mask
$mask-color: () !default;
$mask-color: map.merge(
(
'': rgba(255, 255, 255, 0.9),
'extra-light': rgba(255, 255, 255, 0.3),
),
$mask-color
);
$input-height: () !default;
$input-height: map.merge($common-component-size, $input-height);
map 是一种键值对的数据结构,可以用来存储和操作相关联的数据。它类似于编程语言中的字典或对象,允许使用键(key)来快速访问对应的值(value) 。map.deep-merge 可以实现深度合并变量(嵌套对象)。map.get($map,key) 获取对应的值。mix(color,number) 这个函数可以将某个颜色按照百分比来合成一个新的颜色。
3. Sass 混入动态生成 CSS 变量
@use './function.scss' as *;
// set css var value, because we need translate value to string
// for example:
// @include set-css-var-value(('color', 'primary'), red);
// --el-color-primary: red;
@mixin set-css-var-value($name, $value) {
#{joinVarName($name)}: #{$value};
}
// @include set-css-var-type('color', 'primary', $colors);
// --el-color-primary: #{map.get($colors, 'primary','base')};
@mixin set-css-var-type($name, $type, $variables) {
#{getCssVarName($name,$type)}: #{map.get($variables, $type)};
}
/** set-css-color-type($colors,'primary')
* --fz-color-primary: #409eff;
* --fz-color-primary-light-3: #79bbff;
* --fz-color-primary-light-4: #8cc5ff;
* --fz-color-primary-light-7: #b3d8ff;
* --fz-color-primary-light-8: #c6e2ff;
* --fz-color-primary-light-9: #d9ecff;
*/
@mixin set-css-color-type($colors, $type) {
@include set-css-var-value(('color', $type), map.get($colors, $type, 'base'));
@each $i in (3, 4, 7, 8, 9) {
@include set-css-var-value(
('color', $type, 'light', $i),
map.get($colors, $type, 'light-#{$i}')
);
}
}
// Dynamically generates CSS variables for a component based on a map of attributes and values.
// @include set-component-css-var('button', (
// 'default': #fff,
// 'hover': #f5f5f5,
// 'active': #e6e6e6
// ));==>
// {
// --button: #fff;
// --button-hover: #f5f5f5;
// --button-active: #e6e6e6;
// }
@mixin set-component-css-var($name, $variables) {
@each $attribute, $value in $variables {
@if $attribute== 'default' {
#{getCssVarName($name)}: #{$value};
} @else {
#{getCssVarName($name,$attribute)}: #{$value};
}
}
}
// Generates a CSS variable for the RGB values of a specified color type.
// for example:
// @include set-css-color-rgb('primary')
// --fz-color-primary-rgb: 64, 158, 255;
@mixin set-css-color-rgb($type) {
$color: map.get($colors, $type, 'base');
@include set-css-var-value(
('color', $type, 'rgb'),
#{color.channel($color, 'red'),
color.channel($color, 'green'),
color.channel($color, 'blur')}
);
}
// generate css var from existing css var
// for example:
// @include css-var-from-global(('button', 'text-color'), ('color', $type))
// --el-button-text-color: var(--el-color-#{$type});
@mixin css-var-from-global($var, $gVar) {
$varName: joinVarName($var);
$gVarName: joinVarName($gVar);
#{$varName}: var(#{$gVarName});
}
3.1. 设置 CSS 变量的值
以下代码定义了一个混入 set-css-var-value,用于生成基本的 CSS 变量:
@include set-css-var-value(('color', 'primary'), red);
输出
--el-color-primary: red;
此混入通过 joinVarName 方法将 $name 转换为 CSS 变量的名称,然后将 $value 赋值给该变量。
3.2. 设置特定类型的 CSS 变量
set-css-var-type 混入可以根据一个类型名称和变量表生成 CSS 变量:
@mixin set-css-var-type($name, $type, $variables) {
#{getCssVarName($name, $type)}: #{map.get($variables, $type)};
}
@include set-css-var-type('color', 'primary', $colors);
输出
--el-color-primary: #409eff;
此混入结合 getCssVarName 和 map.get,可以通过 getCssVarName 来生成任何想要生成的变量名成,通过 map.get 来获取指定的键值给变量名赋值。
3.3. 基于颜色的动态变量生成
对于某些场景,例如颜色管理,我们需要定义多个与颜色相关的 CSS 变量。
set-css-color-type 是一个复杂的混入,用于生成一系列颜色变量:
@mixin set-css-color-type($colors, $type) {
@include set-css-var-value(('color', $type), map.get($colors, $type, 'base'));
@each $i in (3, 4, 7, 8, 9) {
@include set-css-var-value(
('color', $type, 'light', $i),
map.get($colors, $type, 'light-#{$i}')
);
}
}
@include set-css-color-type($colors, 'primary');
输出
--el-color-primary: #409eff;
--el-color-primary-light-3: #79bbff;
--el-color-primary-light-4: #8cc5ff;
--el-color-primary-light-7: #b3d8ff;
--el-color-primary-light-8: #c6e2ff;
--el-color-primary-light-9: #d9ecff;
此混入不仅生成了基础颜色变量,还生成了多个与亮度相关的颜色变量,增加了颜色的丰富度。
3.4. 组件样式变量的动态生成
在组件开发中,为每个状态(例如默认、悬停、激活)生成 CSS 变量非常常见。
使用 set-component-css-var 混入,我们可以动态生成组件的 CSS 变量:
@mixin set-component-css-var($name, $variables) {
@each $attribute, $value in $variables {
@if $attribute == 'default' {
#{getCssVarName($name)}: #{$value};
} @else {
#{getCssVarName($name, $attribute)}: #{$value};
}
}
}
@include set-component-css-var('button', (
'default': #fff,
'hover': #f5f5f5,
'active': #e6e6e6
));
输出
--button: #fff;
--button-hover: #f5f5f5;
--button-active: #e6e6e6;
此混入通过判断 default 属性,分别为默认值和其他状态生成对应的 CSS 变量。
3.5. 生成 RGB 格式的 CSS 变量
在某些场景下,CSS 变量需要存储 RGB 格式的值。
@mixin set-css-color-rgb($type) {
$color: map.get($colors, $type, 'base');
@include set-css-var-value(
('color', $type, 'rgb'),
#{color.channel($color, 'red'),
color.channel($color, 'green'),
color.channel($color, 'blue')}
);
}
@include set-css-color-rgb('primary');
color.channel() 是用来获取某个颜色对应的三原色的rgb数值。
输出
--el-color-primary-rgb: 64, 158, 255;
3.6. 基于全局变量生成 CSS 变量
有时候需要从已有的 CSS 变量派生出新的变量。
@mixin css-var-from-global($var, $gVar) {
$varName: joinVarName($var);
$gVarName: joinVarName($gVar);
#{$varName}: var(#{$gVarName});
}
@include css-var-from-global(('button', 'text-color'), ('color', 'primary'));
输出
--el-button-text-color: var(--el-color-primary);
4. 将 css 变量赋值注册在 root 下
前面我们定义了那么多变量值,现在我们需要将这些变量值全部注册到根样式下,这样在其他组件使用 var(xxx)变量名是可以直接访问到对应变量的样式。我们需要将packages\theme-chalk\src\common\var.scss 下所有组件的公共变量全部声明在 root 下。
@use 'common/var' as *;
@use 'mixins/var.scss' as *;
@use 'mixins/mixins.scss' as *;
//common
:root {
@include set-css-var-value('color-white', $color-white);
@include set-css-var-value('color-black', $color-black);
// get rgb
@each $type in (primary, success, warning, danger, error, info) {
@include set-css-color-rgb($type);
}
//Typography
@include set-component-css-var('font-size', $font-size);
@include set-component-css-var('font-weight', $font-family);
// --el-border-radius-#{$type}
@include set-component-css-var('border-radius', $border-radius);
}
// for light
: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);
}
// color-scheme
// Background --el-bg-color-#{$type}
@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);
// Fill --el-fill-color-#{$type}
@include set-component-css-var('fill-color', $fill-color);
@include set-component-css-var('mask-color', $mask-color);
// Border
@include set-css-var-value('border-width', $border-width);
@include set-css-var-value('border-style', $border-style);
@include set-css-var-value('border-color-hover', $border-color-hover);
@include set-css-var-value(
'border',
getCssVar('border-width') getCssVar('border-style')
getCssVar('border-color')
);
}
这些就是组件库中利用 Sass 来设计组件级的样式,希望对你有所帮助。
四、总结
本篇主要是详细的介绍了 Element Plus 中样式的命名规范和设计思路并讲述了如何实现全局样式共享的内容。希望你能有所收获。
愿诸君越来越好,一起进步。