CSS设计模式👾 | BEM 命名规范🤖

242 阅读3分钟

前言

平时我们在写项目的时候似乎都不太重视“命名”这个似乎不在技术范畴之内的东西🤔,随着项目更新迭代变得庞大,维护起来欲哭无泪。尤其是在CSS中,一个高效的命名规范到底有多重要?

BEM命名的核心就是可以清晰的描述页面的结构,从其名字就可以知道某个标记的含义,👀通过查看class属性就可以知道元素之间的关联。

BEM是什么

BEM 是由 Yandex 团队提出的一种 CSS 命名方法论,即 Block(块)、Element(元素)、和 Modifier(修改器)的简称

在选择器中,由以下三种符号来表示扩展的关系:

-(中划线):仅作为连接字符使用,表示某个块或者子元素的多个单词之间的链接符号。
__(双下划线):用来链接块与块的子元素
--(双中线):用来链接块元素与修饰符

举个栗子🌰

人            #Block
人__手          #Element
人__手--小手      #Modifier
人__手--大手      #Modifier
人__脚          #Element

人--男人        #Modifier
人--男人__手      #Element
人--男人__脚      #Element

人--女人        #Modifier
人--女人__手      #Element
人--女人__脚      #Element

放在代码中

.aside {} 
.aside__toggle--show {} 
.aside__toggle--hide {} 
.aside__menu {} 
.aside__menu__item {}

当然这些规则并不一定需要严格遵守的,也可以根据你的团队风格进行修改。

通过 JS 生成 BEM 规范名称

从上文看来,BEM好像确实让代码结构更加清晰有意义,使维护变得简单

但是需要经常写 - 、 __ 、 --,那么就会变得非常繁琐😤

那我们可不可以通过 JavaScript 按照 BEM 命名规范进行动态生成呢🧐

答案当然是可以的,我们以element Plus为例

BEM 命名字符拼接函数

// BEM 命名字符拼接函数
const _bem = (
  namespace: string,
  block: string,
  blockSuffix: string,
  element: string,
  modifier: string
) => {
  // 默认是 Block
  let cls = `${namespace}-${block}`
  // 如果存在 Block 后缀,也就是 Block 里面还有 Block,例如:el-form 下面还有一个 el-form-item
  if (blockSuffix) {
    cls += `-${blockSuffix}`
  }
  // 如果存在元素
  if (element) {
    cls += `__${element}`
  }
  // 如果存在修改器
  if (modifier) {
    cls += `--${modifier}`
  }
  return cls
}

通过 BEM 命名字符拼接函数,我们就可以自由组合生成各种符合 BEM 规则的 classname 了。

export const useNamespace = (block: string) => {
  // 命名前缀也就是命名空间
  const namespace = computed(() => defaultNamespace)
  // 创建块 例如:el-form
  const b = (blockSuffix = '') =>
    _bem(unref(namespace), block, blockSuffix, '', '')
  // 创建元素 例如:el-input__inner
  const e = (element?: string) =>
    element ? _bem(unref(namespace), block, '', element, '') : ''
  // 创建块修改器 例如:el-form--default
  const m = (modifier?: string) =>
    modifier ? _bem(unref(namespace), block, '', '', modifier) : ''
  // 创建后缀块元素 例如:el-form-item
  const be = (blockSuffix?: string, element?: string) =>
    blockSuffix && element
      ? _bem(unref(namespace), block, blockSuffix, element, '')
      : ''
  // 创建元素修改器 例如:el-scrollbar__wrap--hidden-default
  const em = (element?: string, modifier?: string) =>
    element && modifier
      ? _bem(unref(namespace), block, '', element, modifier)
      : ''
  // 创建块后缀修改器 例如:el-form-item--default
  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)
      : ''
  // 创建动作状态 例如:is-success is-required
  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,
    em,
    bm,
    bem,
    is,
  }
}

最后我们就可以在组件中引入 BEM 命名空间函数进行创建各种符合 BEM 命名规范的 classname 了,例如:

  • 创建块 el-form、
  • 创建元素 el-input__inner、
  • 创建块修改器 el-form--default、
  • 创建块后缀元素 el-form-item、
  • 创建元素修改器 el-scrollbar__wrap--hidden-default、
  • 创建动作状态 例如:is-success is-required

具体创建代码使用代码如下:

import {
  useNamespace,
} from '@element-plus/hooks'
// 创建 classname 命名空间实例
const ns = useNamespace('button')

然后就可以在 template 中进行使用了:

<template>
  <button ref="_ref" :class="[ns.b()]">按钮</button>
<template>

image.png

通过 SCSS 生成 BEM 规范样式

既然可以通过 JavaScript 按照 BEM 命名规范进行动态生成类名

那可以通过 SCSS 按照 BEM 命名规范进行动态生成样式吗🧐

Element Plus 的样式采用 SCSS 编写的,那么就可以通过 SCSS 的 @mixin 指令定义 BEM 规范样式。在 mixins 目录下新建三个文件:config.scss、function.scss、mixins.scss。 其中 config.scss 文件编写 BEM 的基础配置比如样式名前缀、元素、修饰符、状态前缀:

$namespace: 'el' !default; // 所有的组件以el开头,如 el-input
$common-separator: '-' !default; // 公共的连接符
$element-separator: '__' !default; // 元素以__分割,如 el-input__inner
$modifier-separator: '--' !default; // 修饰符以--分割,如 el-input--mini
$state-prefix: 'is-' !default; // 状态以is-开头,如 is-disabled

在 SCSS 中,我们使用 $+ 变量名:变量 来定义一个变量。在变量后加入 !default 表示默认值。给一个未通过 !default 声明赋值的变量赋值,此时,如果变量已经被赋值,不会再被重新赋值;但是如果变量还没有被赋值,则会被赋予新的值。

mixins.scss 文件编写 SCSS 的 @mixin 指令定义的 BEM 代码规范。

定义 Block:

@mixin b($block) {
  $B: $namespace + '-' + $block !global;

  .#{$B} {
    @content;
  }
}

$B 表示定义一个一个变量,$namespace 是来自 config.scss 文件中定义的变量, !global 表示其是一个全局变量,这样就可以在整个文件的任意地方使用。#{} 字符串插值,类似模板语法。通过 @content 可以将 include{} 中传递过来的内容导入到指定位置。

定义 Element:

@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;
      }
    }
  }
}

首先定义一个全局变量 $E,接着定义父选择器 $selector,再定义当前的选择器 $currentSelector,再通过循环得到当前的选择器。接着通过函数 hitAllSpecialNestRule(hitAllSpecialNestRule 函数在 mixins 目录的 function.scss 文件中) 判断父选择器是否含有 Modifier、表示状态的 .is- 和 伪类,如果有则表示需要嵌套。@at-root 的作用就是将处于其内部的代码提升至文档的根部,即不对其内部代码使用嵌套。

定义修改器:

@mixin m($modifier) {
  $selector: &;
  $currentSelector: '';
  @each $unit in $modifier {
    $currentSelector: #{$currentSelector +
      $selector +
      $modifier-separator +
      $unit +
      ','};
  }

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

这个非常好理解,就是定义了父选择器变量 $selector 和 当前选择器变量 $currentSelector,并且当前选择器变量初始值为空,再通过循环传递进来的参数 $modifier,获得当前选择器变量 $currentSelector 的值,再定义样式内容,而样式内容是通过 @contentinclude{} 中传递过来的内容。

定义动作状态:

@mixin when($state) {
  @at-root {
    &.#{$state-prefix + $state} {
      @content;
    }
  }
}

选择器就是 config.scss 文件中的变量 $state-prefix 加传进来的状态变量,而样式内容是通过 @contentinclude{} 中传递过来的内容。

接着我们再看下上面定义 Element 的时候说到的 hitAllSpecialNestRule 函数,这个函数是定义在 mixins 目录下的 function.scss 文件中。function.scss 文件内容如下:

@use 'config';

// 该函数将选择器转化为字符串,并截取指定位置的字符
@function selectorToString($selector) {
  $selector: inspect(
    $selector
  ); // inspect(...) 表达式中的内容如果是正常会返回对应的内容,如果发生错误则会弹出一个错误提示。
  $selector: str-slice($selector, 2, -2); // str-slice 截取指定字符
  @return $selector;
}
// 判断父级选择器是否包含'--'
@function containsModifier($selector) {
  $selector: selectorToString($selector);

  @if str-index($selector, config.$modifier-separator) {
    // str-index 返回字符串的第一个索引
    @return true;
  } @else {
    @return false;
  }
}
// 判断父级选择器是否包含'.is-'
@function containWhenFlag($selector) {
  $selector: selectorToString($selector);

  @if str-index($selector, '.' + config.$state-prefix) {
    @return true;
  } @else {
    @return false;
  }
}
// 判断父级是否包含 ':' (用于判断伪类和伪元素)
@function containPseudoClass($selector) {
  $selector: selectorToString($selector);

  @if str-index($selector, ':') {
    @return true;
  } @else {
    @return false;
  }
}
// 判断父级选择器,是否包含`--` `.is-`  `:`这三种字符
@function hitAllSpecialNestRule($selector) {
  @return containsModifier($selector) or containWhenFlag($selector) or
    containPseudoClass($selector);
}

通过上述代码我们就可以知道 hitAllSpecialNestRule 函数是如何判断父选择器是否含有 Modifier、表示状态的 .is- 和 伪类的了。