likes
comments
collection
share

浅解ElementPlus的BEM实现

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

关于CSS命名方法论

BEM是一种前端CSS命名方法论,此外还有OOCSSSMACSSSUIT CSSAtomic等方法论。

OOCSS(Object-Oriented CSS) 称为“面向对象CSS”,在OOCSS中,使用类(class)而不是具体的元素选择器来定义样式规则。将样式分为两个独立部分:结构(定义元素的布局,位置)、皮肤(定义元素的外观样式)。通过结构与皮肤的分离,实现样式的复用性和灵活性。

SMCSS(Scalable and Modular Architecture for CSS)将CSS样式分为5种规则类别:

  1. 基础(Base):包含基本的HTML元素样式,如标题、段落、链接等。这些样式是全局的,适用于整个网站。

  2. 布局(Layout):定义了页面的整体布局,如头部、侧边栏、网格系统等。布局样式与具体的内容无关,负责处理结构和布局。

  3. 模块(Module):将页面拆分为独立的模块,每个模块有自己独立的样式。模块应该是可重复使用的,可以在不同的页面中使用。

  4. 状态(State):处理状态和交互效果,例如悬停、激活、隐藏和展开等。这些样式适用于元素的不同状态,而不是元素本身的样式。

  5. 主题(Theme):定义了不同的视觉主题或样式变种,允许为网站轻松切换不同的外观风格

SUIT CSS(Simple, Understandable, Independent, Testable CSS)是一种组件开发的CSS方法论,强调一致性和可组合性。

主要特点:

  1. 命名规则:SUIT CSS 提供了一种清晰、明确的 CSS 类名命名规则,便于开发者快速理解代码的结构和功能。

  2. 可重用组件:SUIT CSS 设计的目标是创建可重用、可组合的组件。这使得代码更加模块化,有利于维护和测试。

  3. 自定义:SUIT CSS 支持使用预处理器(如 PostCSS)进行自定义和扩展,使其更好地适应项目需求。

  4. 独立性:SUIT CSS 的另一个重要目标是确保每个组件都是独立的,这意味着它们不应依赖于其他 CSS 规则或 DOM 结构。这样一来,组件可以在不同的上下文中使用,而不会出现样式冲突。

  5. 明确性:SUIT CSS 强调代码应该易于理解和维护。它采用了一些最佳实践,如使用明确的命名规约和避免过度嵌套,以提高代码的可读性和可维护性。

Atomic,它的核心思想是将样式属性拆分成小的、独立的类,然后通过组合这些类来创建样式规则。这些类通常只包含一个样式属性,并使用直观的命名约定,以便于理解和重用。例如Tailwind CSS

关于BEM

  1. 块(Block):块是指页面上独立的,具有某个功能或意义的元素。出现块嵌套时使用 ' - ' 来连接,例如表单(form)。
  2. 元素(Element):作为块的组成部分,语义上与块相关联,只有在块中的上下文中才具有意义。使用 ' __ ' 连接符进行连接,例如(form-item__context),
  3. 修饰符(Modifier):修饰符用于修改块或元素的外观、状态或行为。使用 ' -- ' 来连接, 例如 button--primary
  4. 在BEM方法论中,块的样式从不依赖页面的其他元素,所以不会出现级联问题(CSS的优先级/权重问题)
  5. 以不同的方式去组合块,支持灵活的复用,同样在进行样式覆盖时也不用过多考虑样式冲突的问题
  6. 清晰且结构化的命名规范也让代码易于理解,维护和复用

利用js实现BEM命名的使用规范

在ElemenPlus中,组件样式使用 命名空间hook(useNameSpace) 来编写绑定。利用js来规范命名的使用,严格确保了BEM的命名规范。

// 相关具体代码路径 ElementPlus仓库 \packages\hooks\use-namespace\index.ts
import { computed, unref } from 'vue'
export const defaultNamespace = 'el' // 定义默认命名前缀
const statePrefix = 'is-' // 定义状态修饰符

/** BEM命名字符拼接函数
 * @param namespace - 命名空间
 * @param block - 块
 * @param blockSuffix - 块嵌套
 * @param element - 元素
 * @param modifier - 修饰符
 * */ 
const _bem = (
  namespace: string,
  block: string,
  blockSuffix: string,
  element: string,
  modifier: string
) => {
  // 基础的Block命名 例如 el-button
  let cls = `${namespace}-${block}`
  // 判断是否存在Block嵌套的情况,如果存在则添加 例如 el-form-item
  if (blockSuffix) {
    cls += `-${blockSuffix}`
  }
  // 判断是否存在元素内嵌的情况,如果存在则添加 例如 el-form-item__content
  if (element) {
    cls += `__${element}`
  }
  // 判断是否存在修饰符,如果存在则添加 例如 el-button--primary
  if (modifier) {
    cls += `--${modifier}`
  }
  // 返回命名
  return cls
}

/**命名空间的使用
 * @param block - 块
 */
export const useNameSpace = (block: string) => {
  const namespace = computed(() => defaultNamespace)

  /** 创建块命名
   * @param blockSuffix - 嵌套块的名称
   * @return 块命名 例如 el-button / el-button-context
   */
  const b = (blockSuffix = '') => _bem(unref(namespace), block, blockSuffix, '', '')

  /** 创建元素命名 
   * @param element - 元素名称
   * @returns 元素命名 例如 el-button__context / el-input__inner
   */
  const e = (element?: string) => element ? _bem(unref(namespace), block, '', element, '') : ''

  /** 创建块修饰符命名 
   * @param modifier - 修饰符名称
   * @returns 块修饰符命名 例如 el-button--primary
   */
  const m = (modifier?: string) => modifier ? _bem(unref(namespace), block, '', '', modifier) : ''

  /** 创建嵌套块的元素命名
   * @param blockSuffix - 嵌套块的名称
   * @param element - 元素名称
   * @returns 嵌套块的元素名称 例如 el-form-item__context
   * 只传入一个参数时不返回任何参数
   */
  const be = (blockSuffix?: string, element?: string) =>
    blockSuffix && element ? _bem(unref(namespace), block, blockSuffix, element, '') : ''

  /** 创建元素修饰符命名
   * @param element - 元素名称
   * @param modifier - 修饰符名称
   * @returns 元素修饰符的名称 例如 el-button__context--selected(只传入一个参数时不返回任何参数)
   */
  const em = (element?: string, modifier?: string) =>
    element && modifier ? _bem(unref(namespace), block, '', element, modifier) : ''

  /** 创建嵌套块修饰符的命名
   * @param blockSuffix - 嵌套块的名称
   * @param modifier - 修饰符名称
   * @returns 嵌套块修饰符名称 例如 el-form-item--focused
   */
  const bm = (blockSuffix?: string, modifier?: string) =>
    blockSuffix && modifier ? _bem(unref(namespace), block, blockSuffix, '', modifier) : ''

  // 创建块后缀元素修饰器 例如 el-form-item__content--xxx
  const bem = (blockSuffix?: string, element?: string, modifier?: string) =>
    blockSuffix && element && modifier ?
    _bem(unref(namespace), block, blockSuffix, element, modifier) : ''

  /** 创建状态命名
   * @param name - 状态名称
   * @param args - 状态值
   * @returns 状态名称 例如 is-disabled
   */
  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}` : ''
  }
  return {
    namespace,
    b,
    e,
    m,
    be,
    bm,
    em,
    bem,
    is
  }
}

浅解ElementPlus的BEM实现

上图为el-button的类名绑定,衍生出的CSS名称:el-button、el-button--primary(el-button--{type})、el-button--small(el-button--{size})、is-disabled、is-loading、is-plain、is-round、is-circle、is-text、is-link、is-has-bg

使用SCSS构建BEM规范

通过 button.scss 渐进性学习ElementPlus对于SCSS的使用。

  • 首先观察该scss文件存在哪些文件依赖

浅解ElementPlus的BEM实现

依赖:

  1. sass中的map模块
  2. common/var.scss (放置CSS样式的变量映射构建,以及组件样式所需的变量映射构建)
  3. mixins/_button.scss ( 放置button样式的一些mixins,便于代码复用。根据用户定义的button类型,使用相应样式)
  4. mixins/mixins.scss (放置的是一些工具性的mixins。其中BEM的相关mixins,就是我们需要关注的重点)
  5. mixins/utils.scss (放置通用的样式处理mixins)
  6. mixins/_var.scss (放置使用CSS变量的辅助mixins)

分析模块联系

在ElementPlus中,common/var模块存放着相关变量映射构建,其中的基础变量会在 \packages\theme-chalk\src\var.scss 中声明到顶部作用域供全局使用这些CSS变量。而组件样式使用的变量映射则在对应组件样式文件中导入暴露。变量映射的封装提高了代码复用性,及组合的灵活性,样式变量对应性。这些变量配置构建了ElemenPlus的组件风格

在ElementPlus中,mixins/_var.scss存放着一系列关于CSS变量使用的mixins。辅助CSS变量的全局声明,以及变量的使用。

@use "sass:map";

@use "config" as *;
@use "../common/var" as *;
@use "function" as *;

// 设置css变量
// 例: @include set-css-var-color-value(('button', 'primary'), red)
// => --el-color-primary: red
@mixin set-css-var-value($name, $value) {
  #{joinVarName($name)}: #{$value};
}

// 例: @include set-css-var-type('color', 'primary', $map)
// => --el-color-primary: #{map.get($map, 'primary')}
@mixin set-css-var-type($name, $type, $variables) {
  #{getCssVarName($name, $type)}: #{map.get($variables, $type)};
}

@mixin set-css-color-type($colors, $type) {
  @include set-css-var-value(("color", $type), map.get($colors, $type, "base"));

  @each $i in (3, 5, 7, 8, 9) {
    @include set-css-var-value(("color", $type, "light", $i), map.get($colors, $type, "light-#{$i}"));
  }

  @include set-css-var-value(("color", $type, "dark-2"), map.get($colors, $type, "dark-2"));
}

// set all css var for component by map
// 将组件使用到的CSS变量关联到组件中
@mixin set-component-css-var($name, $variables) {
  @each $attribute, $value in $variables {
    @if $attribute== "default" {
      #{getCssVarName($name)}: #{$value};
    } @else {
      #{getCssVarName($name, $attribute)}: #{$value};
    }
  }
}

// 设置rgb颜色变量  => --cl-color-${type}-rgb 
// --el-color-primary-rgb
@mixin set-css-color-rgb($type) {
  $color: map.get($colors, $type, "base");
  @include set-css-var-value(("color", $type, "rgb"), #{red($color), green($color), blue($color)});
}

// 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});
// 根据原CSS变量创建新的CSS变量
@mixin css-var-from-global($var, $gVar) {
  $varName: joinVarName($var);
  $gVarName: joinVarName($gVar);
  #{$varName}: var(#{$gVarName});
}

在ElementPlus中,mixins/mixins.scss模块存放着BEM实现的相关mixins。

该模块依赖于 mixins/config.scssmixins/function.scss这两个模块。其中config模块放置的是一些BEM规范实现的一些前缀、连接符声明。

浅解ElementPlus的BEM实现

fuction模块放置的是关于实现及使用BEM规范的一些辅助函数。

// mixins/function.scss
@use "config";

// BEM support Func
// 将类选择器转换为一个字符串 并截取指定位置的字符
@function sclectorToString($sclector) {
  $sclector: inspect($sclector); // inspect(...) 表示 如果参数内容正常则直接返回 反之抛出错误
  $sclector: str-slice($sclector, 2, -2); // str-slice() 用于截取指定字符
  @return $sclector;
}

// 判断父级选择器是否包含 '--' 修饰符
@function containsModifier($sclector) {
  $sclector: sclectorToString($sclector);

  // str-index() 用于返回指定字符的位置 如果未查找到 则返回null
  @if str-index($sclector, config.$modifier-separator) {
    @return true;
  } @else {
    @return false;
  }
}

// 判断父级选择器是否包含 '.is'
@function containWhenFlag($sclector) {
  $sclector: sclectorToString($sclector);

  @if str-index($sclector, "." + config.$state-prefix) {
    @return ture;
  } @else {
    @return false;
  }
}

// 判断父级选择器是否包含 ':' (用于判断伪类和伪元素)
@function containPseudoClass($sclector) {
  $sclector: sclectorToString($sclector);

  @if str-index($sclector, ":") {
    @return true;
  } @else {
    @return false;
  }
}

// 用于判断父级选择器 是否包含 '--', '.is', ':' 这三种字符
@function hitAllSpecialNestRule($sclector) {
  @return containsModifier($sclector) or containWhenFlag($sclector) or containPseudoClass($sclector);
}

// join var name
// joinVarName(('button', 'text-color')) => '--cl-button-text-color'
// 使用: 以括号包裹参数:(组件类型名称, 变量名称, ...)
@function joinVarName($list) {
  // 拼接出css变量名称
  $name: '--' + config.$namespace;
  @each $item in $list {
    @if $item != '' {
      $name: $name + '-' + $item;
    }
  }
  @return $name;
}

// getCssVarName('button', 'text-color') => '--cl-button-text-color'
// 使用: 参数一:组件类型名称 参数二:变量名称 ....
@function getCssVarName($args...) {
  // 获取css变量名称
  @return joinVarName($args);
}

// getCssVar('button', 'text-color') => var(--cl-button-text-color)
// 使用: 参数一:组件类型名称 参数二:变量名称 ....
@function getCssVar($args...) {
  // 使用Css变量
  @return var(#{joinVarName($args)});
}

// getCssVarWithDefault(('button', 'text-color'), red) => var(--cl-button-text-color, red)
@function getCssVarWithDefault($args, $default) {
  // 设置Css变量默认值
  @return var(#{joinVarName($args)}, #{$default});
}

// bem('block', 'clement', 'modifier') => 'cl-block__clement--modifier'
@function bem($block, $clement: "", $modifier: "") {
  $name: config.$namespace + config.$common-separator + $block;

  @if $clement != "" {
    $name: $name + config.$clement-separator + $clement;
  }

  @if $modifier != "" {
    $name: $name + config.$modifier-separator + $modifier;
  }

  // @debug $name;
  @return $name;
}

关于BEM使用的相关mixins

// mixins/mixins.scss
@use "config" as *;
@use 'function' as *;

// BEM规范
// 定义Block
@mixin b($block) {
  $B: $namespace + $common-separator + $block !global; // !global 标识 $B 这个变量为全局变量

  // #{} 表示字符串插值 @content表示内容占位 通过include{}接收所传递的内容
  .#{$B} {
    @content;
  }
}

// 定义Element
@mixin e($element) {
  $E: $element !global;
  $selector: &; // 定义父选择器
  $currentSelector: ""; // 定义当前选择器

  // 通过循环获取当前的选择器
  @each $unit in $element {
    $currentSelector: #{$currentSelector + "." + $B + $element-separator + $unit + ","};
  }

  // hitAllSpecialNestRule 用于判断父选择器是否含有Modifier、表示状态的 .is- 和 伪类,如果存在则表示需要进行嵌套
  @if hitAllSpecialNestRule($selector) {
    // @at-root 作用是将处于其内部的代码提升到文档的根部 即不对其内部代码使用嵌套
    @at-root {
      #{$selector} {
        #{$currentSelector} {
          @content;
        }
      }
    }
  } @else {
    @at-root {
      #{$currentSelector} {
        @content;
      }
    }
  }
}

// 定义Modifier
@mixin m($modifier) {
  $selector: &;
  $currentSelector: "";

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

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

// 定义状态 必须在父级容器的包裹下使用
@mixin when($state) {
  @at-root {
    &.#{$state-prefix + $state} {
      @content;
    }
  }
}

上图所示 @mixins b , @mixins e , @mixins m@mixins when,分别对应 block模块,element元素,modifier修饰符,状态修饰。在编写样式使用这些mixins来实现BEM的规范。根据这套规则,构建者可以不必过多考虑如何统一标准使用BEM。学会规范使用即可。 关于使用,回归到 button.scss文件中。

浅解ElementPlus的BEM实现

浅解ElementPlus的BEM实现

可以看到样式值都是搭配mixins/_var中的 @mixins getCSSVar使用。让构建者不用自主编写复杂的变量名称。避免变量名称的编写错误。BEM的命名则是搭配 mixins/mixins中的相关mixins声明实现。

总结

在ElementPlus中,为了BEM的实现,构建了一套完整的规则体系,如CSS变量的封装,CSS变量的使用规范,命名规范等。根据这些规则体系去编写样式很好地践行了BEM方法论。在多人协作开发的情况下,有这样一套规范,统一了编码风格。但这套规则体系下,参与组件开发之前需要对这套规则体系有一定的熟悉才能够灵活地去运用。存在一定的心智负担。不过完善的规范都是需要一定的心智负担去维护的。(以上内容仅个人浅薄理解

浅解ElementPlus的BEM实现

参考来源

BEM

转载自:https://juejin.cn/post/7276675247597862923
评论
请登录