elementPlus 源码阅读 - 样式篇(1)

1,113 阅读3分钟

巧妙的封装 BEM

el 通过mixin 实现了 BEM (命名空间+模块 + 元素名 + 修饰器名) namespace-block__element--modifier

// 使用方式
.test-bem {
    color: purple;
    @include b(heade1r) {
        color:orange;
        @include e((aa,bb,cc)) {
            color:black;
            @include m (success) {
                background: green;
            }
        }
    }
}

// 编译后的结果
.test-bem {
  color: purple;
}
.test-bem .el-heade1r {
  color: orange;
}
.el-heade1r__aa, .el-heade1r__bb, .el-heade1r__cc {
  color: black;
}
.el-heade1r__aa, .el-heade1r__bb, .el-heade1r__cc--success {
  background: green;
}

BEM 的封装

  1. 定义配置相关的变量
$namespace: 'el'; // 命名空间
$element-separator: '__'; // 元素分隔符
$modifier-separator: '--'; // 修饰器分隔符
  1. 实现 block
// block
@mixin b ($block) {
  // !global 将局部变量转为全局变量
  $B: $namespace + '-' + $block !global;
  // 模板语法
  .#{$B} {
    @content;
  }
}

// 示例

.test-bem {
    color: purple;
    @include b(heade1r) {
        color:orange;
    }
    
}
// 编译后
.test-bem {
  color: purple;
}
.test-bem .el-heade1r {
  color: orange;
}
  1. 实现 e mixin:

    如果 包含修饰器 状态 或者 伪类 创建一个 与父类同级的 以 $element-separator + element 结尾的类

    $element 可以是单个参数 或者 list

    该 mixin 依赖与 block mixin 创建的 $B

@mixin e($element) {
  $E: $element !global;
  $selector: &; // & 表示父类选择器
  $currentSelector: "";

  @each $unit in $element {
    $currentSelector: #{$currentSelector + '.' + $B + $element-separator + $unit + ','}
  }

  @if hitAllSpecialNestRule($selector) {
    // 如果 包含修饰器 状态 或者 伪类 则
    // 返回 与父类同级
    @at-root {
      #{$selector} {
        #{$currentSelector} {
          @content;
        }
      }
    }
  } @else {
    @at-root {
      #{$currentSelector} {
        @content;
      }
    }
  }
}

// 判断是否存在特殊嵌套 包含修饰器,包含状态 包含伪类
@function hitAllSpecialNestRule($selector) {
  @return containsModifier($selector) or containWhenFlag($selector) or containPseudoClass($selector);
}

@function selectorToString($selector) {
  // inspect($value) 将列表转换成字符串
  $selector: inspect($selector);
  // str-slice($string, $start-at, $end-at) 截取字符串
  $selector: str-slice($selector, 2, -2);
  @return $selector;
}

// 选择器是否包含修饰符
@function containsModifier($selector) {
  $selector: selectorToString($selector);

  // 检查 选择器中 是否存在修饰器分隔符
  @if str-index($selector, $modifier-separator) {
    @return true;
  } @else {
    @return false;
  }
}

@function containWhenFlag($selector) {
  $selector: selectorToString($selector);
  // 是否包含状态前缀
  @if str-index($selector, '.' + $state-prefix) {
    @return true
  } @else {
    @return false
  }
}

@function containPseudoClass($selector) {
  $selector: selectorToString($selector);

  // 是否包含伪类
  @if str-index($selector, ':') {
    @return true
  } @else {
    @return false
  }
}

示例代码

// 示例1
.test-bem{
    color: purple;
    @include b(heade1r) {
        color:orange;
    }
    @include e((aa,bb,cc)) {
        color:black;
    }
}
// 编译结果
.test-bem {
  color: purple;
}
.test-bem .el-heade1r {
  color: orange;
}

.el-heade1r__aa, .el-heade1r__bb, .el-heade1r__cc {
  color: black;
}

// 示例2
.test-bem--modify{
    color: purple;
    @include b(heade1r) {
        color:orange;
        
    }
    @include e((aa,bb,cc)) {
        color:black;
    }
}
// 编译结果
.test-bem--modify {
  color: purple;
}
.test-bem--modify .el-heade1r {
  color: orange;
}
.test-bem--modify .el-heade1r__aa, .test-bem--modify .el-heade1r__bb, .test-bem--modify .el-heade1r__cc {
  color: black;
}
  1. modifier 实现
@mixin m ($modifier) {
  $selector: &;
  $currentSelector: "";

  @each $unit in $modifier {
    $currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ","};
  }

  @at-root {
    #{$currentSelector} {
      @content;
    }
  }
}

示例代码

.test-bem{
    color: purple;
    @include b(heade1r) {
        color:orange;
    }
    @include e(aa) {
        color:black;
        @include m (success) {
            background: green;
        }
    }
}

// 编译后
.test-bem {
  color: purple;
}
.test-bem .kk-heade1r {
  color: orange;
}
.kk-heade1r__aa {
  color: black;
}
.kk-heade1r__aa--success {
  background: green;
}

CSS 变量封装

CSS 变量有两种形式:

1. 全局变量

:root { // 全局作用域
    // Background --el-bg-color-#{$type}
    @include set-component-css-var('bg-color', $bg-color);
    @include set-component-css-var('border-color', $border-color);
    // --el-text-color-#{$type}
    @include set-component-css-var('text-color', $text-color);
    // Fill --el-fill-color-#{$type}
    @include set-component-css-var('fill-color', $fill-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',
        #{var(--kk-border-width) var(--kk-border-style) var(--kk-border-color)}
	);
}

其中有两个 mixin set-component-css-varset-css--var-value

@include set-component-css-var('bg-color', $bg-color); 示例:

$bg-color: () !default;
$bg-color: map.merge(
    (
        '': #ffffff,
        'page': #ffffff,
        'overlay': #ffffff,
    ),
    $bg-color
);
$namespace: "el";

@include set-component-css-var('bg-color', $bg-color);

// 接收两个参数,name 和 $variables 列表
// --el-bg-color: #ffffff
// --el-bg-color-page: #ffffff
// --el-bg-color-overlay: #ffffff
@mixin set-component-css-var($name, $variables) {
    @each $attribute, $value in $variables {
        #{getCssVarName($name, $attribute)}: #{$value}
    }
}

// getCssVarName('bg-color', '') => '--el-bg-color'
@function getCssVarName($arg...) {
    @return joinVarName($arg)
        }

// 当列表中的元素不为 '' 的时候将其使用 - 连接
@function joinVarName ($list) {
    $name: '--' + $namespace;
    @each $item in $list {
        @if $item != '' {
            $name: $name + '-' + $item;
        }
    }
    @return $name;
}

通过遍历列表的方式,实现了多个全局变量。

set-css-var-value 示例:

$namespace: "el";
$border-width: 1px !default;
@include set-css-var-value('border-width', $border-width);

// border-width
// return --el-border-width: 1px
@mixin set-css-var-value($name, $value) {
    #{joinVarName($name)}: #{$value};
}

// 当列表中的元素不为 '' 的时候将其使用 - 连接
@function joinVarName ($list) {
    $name: '--' + $namespace;
    @each $item in $list {
        @if $item != '' {
            $name: $name + '-' + $item;
        }
    }
    @return $name;
}

2. 局部变量

局部变量的定义 就在当前的组件内部:

$button: () !default;
// map.merge 合并多个map
$button: map.merge(
  (
    'font-weight': getCssVar('font-weight-primary'), // 字体加粗
  ),
  $button
);

// 将参数变成列表
// 使用 var 来获取变量值 (--el-font-weight-primary)
// 这个值在全局作用域 定义的
// :root {
// 	@include set-css-var-value('font-weight-primary', 500);
// }
@function getCssVar ($args...) {
  @return var(#{joinVarName($args)})
}

// 拼接字符串 --el-font-weight-primary
@function joinVarName ($list) {
  $name: '--' + config.$namespace;
  @each $item in $list {
    @if $item != '' {
      $name: $name + '-' + $item;
    }
  }
  @return $name;
}

// el-button
@include b(button) {
  @include set-component-css-var('button', $button);
}

组件添加类名

通过方法 返回字符串的方式来给组件绑定类名。

<button
    :class="[
      ns.b(),
      ns.m(type),
      ns.m(size),
      ns.is('disabled', disabled),
      ns.is('plain', plain),
      ns.is('round', round),
      ns.is('circle', circle),
      ns.is('loading', loading)
    ]"
  >
    <slot />
  </button>

ns 方法实现

const defaultNamespace = 'el'

// 连接字符串
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
}

const useNamespace = (block: string) => {
  // 命名空间
  const namespace = computed(() => defaultNamespace)
  
  // block 接收一个块的前缀 返回 ${namespace}-${block}-${blockSuffix}
  const b = (blockSuffix = '') => _bem(unref(namespace), block, blockSuffix, '''')
  
  // element 接收元素名 返回 ${namespace}-${block}__${element}
  const e = (element?: string) => element ? _bem(unref(namespace), block, '', element, '') : ''
  
  // element 接收元素名 返回 ${namespace}-${block}--${modifier}
  const m = (modifier?: string) => modifier ? _bem(unref(namespace), block, '''', modifier) : ''
  
  // 返回 ${namespace}-${block}-${blockSuffix}__${element}
  const be = (blockSuffix?: string, element?: string) =>
    blockSuffix && element
      ? _bem(unref(namespace), block, blockSuffix, element, '')
      : ''
  
  // 返回 ${namespace}-${block}__${element}--${modifier}
  const em = (element?: string, modifier?: string) =>
    element && modifier
      ? _bem(unref(namespace), block, '', element, modifier)
      : ''
  
  // 返回 ${namespace}-${block}-${blockSuffix}--${modifier}
  const bm = (blockSuffix?: string, modifier?: string) =>
    blockSuffix && modifier
      ? _bem(unref(namespace), block, blockSuffix, '', modifier)
      : ''
  
  // 返回 ${namespace}-${block}-${blockSuffix}__${element}--${modifier}
  const bem = (blockSuffix?: string, element?: string, modifier?: string) =>
    blockSuffix && element && modifier
      ? _bem(unref(namespace), block, blockSuffix, element, modifier)
      : ''
  
  // 如果 name 和 state 存在则返回 is${name} 否则返回 ''
  const is: {
    (name: string, state: boolean | undefined): string
    (name: string): string
  } = (name: string, ...args: [boolean | undefined] | []): string => {
    const state = args.length >= 1 ? args[0]! : true
    return name && state ? `${statePrefix}${name}` : ''
  }
  
  return {
    namespace,
    b,
    e,
    m,
    be,
    em,
    bm,
    bem,
    is
  }
}

总结

  1. 通过BEM 规范实现样式书写

  2. 变量的封装:

    1. 通过 连接字符串 拼接变量;
    2. 通过 :root{} 和 当前组件的作用域 实现 全局变量和组件变量;
    3. 使用 map 快速生成 多个变量;
    4. 通过 var + 字符串拼接的方式获取变量值。
  3. html 元素通过函数生成字符串的方式 绑定类名。