组件化开发必知的CSS命名规范——BEM

1,862 阅读6分钟

一、什么是 BEM

BEM 是由 Yandex 公司推出的一套 CSS 命名规范,官方是这么描述它的:BEM 是一种让你可以快速开发网站并对此进行多年维护的技术。

BEM(Block, Element, Modifier)是一种前端编码规范,用于命名 HTML 和 CSS 中的类和选择器,它旨在提供一种一致的方式来组织和命名代码,使其易于理解、扩展和维护。

以下是 BEM 规范的基本原则:

  1. 块(Block):块是一个独立的可重用组件,它代表一个完整的实体,它是整个 BEM 结构中最高层级的部分,应该有一个唯一的类名。 示例:.el-button、.el-navbar
  2. 元素(Element):元素是块的组成部分,不能独立存在。它们依赖于块的上下文,并且有属于块的类名作为前缀。 示例:.button__text、.navbar__item
  3. 修饰符(Modifier):修饰符用于修改块或元素的外观、状态或行为。它们是可选的,可以单独使用或与块或元素的类名结合使用。 示例:.el-button--large、.el-upload__item--active

二、BEM命名的好处

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

  1. 提供一种一致的命名约定,使团队可以更轻松地理解和维护代码
  2. 促进可重用性和模块化开发
  3. 减少 CSS 的特异性(specificity)问题,避免组件间样式冲突

三、常见的使用情况

.block{}  
.block__element{}  
.block__element--modifier{}  

.block{}  
.block__element-name{}  
.block__element-name--modifier{}

.block-name{}  
.block-name__element{}  
.block-name__element--modifier{}

.block-name{}  
.block-name__element-name{}  
.block-name__element-name--modifier{}

.block--modifier{}

在代码中使用:

<div class="project-list">
    <div class="project-list__item"></div>
    <div class="project-list__item--red"></div>
    <div class="project-list__item--green"></div>
</div>

<style>
    .project-list{}
    .project-list__item{}
    .project-list__item--red{}
    .project-list__item--green{}
</style>

四、BEM解决的问题

CSS 的样式应用是全局性的,没有作用域可言,考虑以下场景:

场景一:开发一个弹窗组件,在现有页面中测试都没问题,一段时间后,新需求新页面,该页面一打开这个弹窗组件,页面中样式都变样了,一查问题,原来是弹窗组件和该页面的样式相互覆盖了,接下来就是修改覆盖样式的选择器...

场景二:承接上文,由于页面和弹窗样式冲突了,所以把页面的冲突样式的选择器加上一些结构逻辑,比如子选择器、标签选择器,借此让选择器独一无二。一段时间后,新同事接手跟进需求,对样式进行修改,由于选择器是一连串的结构逻辑,看不过来,嫌麻烦,就干脆在样式文件最后用另一套选择器,加上了覆盖样式...接下来又有新的需求...最后的结果,一个元素对应多套样式,遍布整个样式文件...

以往开发组件,我们都用“重名概率小”或者干脆起个“当时认为是独一无二的名字”来保证样式不冲突,这是不可靠的。

理想的状态下,我们开发一套组件的过程中,我们应该可以随意的为其中元素进行命名,而不必担心它是否与组件以外的样式发生冲突。

BEM 解决这一问题的思路在于,由于项目开发中,每个组件都是唯一无二的,其名字也是独一无二的,组件内部元素的名字都加上组件名,并用元素的名字作为选择器,自然组件内的样式就不会与组件外的样式冲突了。

这是通过组件名的唯一性来保证选择器的唯一性,从而保证样式不会污染到组件外。这也可以看作是一种“硬性约束”,因为一般来说,我们的组件会放置在同一目录下,那么操作系统中,同一目录下文件名必须唯一,这一点也就确保了组件之间不会冲突。

BEM 的命名规矩很容易记:block-name__element-name--modifier-name,也就是模块名 + 元素名 + 修饰器名。

五、BEM实例

理论说完了,让我们用一个实例(Checkbox组件)来具体说说我在开发时的具体的结构设计和思路:

image.png

1. 确定组件结构和"块"的划分:从组件设计上讲,我们会倾向于用 checkbox.vue 来维护组件,那么我们在编码时,便确定了 front-checkbox 这个顶层"块儿"

<div class="front-checkbox">
    ...
</div>

2. 区分元素:对于一个 chekbox,可以划分的元素就应该是左边的选框 box 和右边的选项文字 label 了,我们可以将选框内部的 icon 图标作为选框内部的一个元素

<div class="front-checkbox">
    <div class="front-checkbox__box">
        <div class="front-checkbox__icon"></div>
    </div>
    <div class="front-checkbox__label">
        {{ name }}
    </div>
</div>

3. 确定修饰器:接下来我们要确定各个元素应当具备几个修饰器、每个修饰器又有几种具体的状态

从设计图上来看, box 这个元素具有禁用(disabled),选中状态(status)这两个修饰;其中,disabled 的取值是 true 和 false 我们通常的做法是取值为 true 的时候为他加上这个修饰;而 status 的状态有三种,分别是未选(normal),选中(active),半选(indeterminate)

image.png

<div class="front-checkbox">
    <div class="front-checkbox__box front-checkbox__box--normal">
        <div class="front-checkbox__icon"></div>
    </div>
    <div class="front-checkbox__label">
        {{ name }}
    </div>
</div>

image.png

<div class="front-checkbox">
    <div class="front-checkbox__box front-checkbox__box--active front-checkbox__box--disabled">
        <div class="front-checkbox__icon"></div>
    </div>
    <div class="front-checkbox__label">
        {{ name }}
    </div>
</div>

六、快速生成BEM

1. 计算属性:

const frontButtonClass = computed(() => {
  return {
    'front-button': true,
    'front-button--loading': props.loading,
    'front-button--long': props.long,
    'front-button--disabled': props.disabled || props.loading,
    [`front-button--${props.status}`]: props.status,
    [`front-button--${props.type}`]: props.type,
    [`front-button--${props.size}`]: props.size,
    [`front-button--${props.shape}`]: props.shape,
  }
})

2. 采用命名方法:

const isObject = (val: unknown): val is object => {
  return val !== null && typeof val === "object";
};

const isArray = (val: unknown): val is string[] => {
  return Array.isArray(val);
};

const isString = (val: unknown): val is string => {
  return typeof val === "string";
};

type BEMElement = string;
type BEMModifier =
  | (string | undefined | false)[]
  | Record<string, boolean | string | undefined>;

const createModifier = (prefixClass: string, modifierObject?: BEMModifier) => {
  let modifiers: string[] = [];
  if (isArray(modifierObject)) {
    modifiers = modifierObject
      .map((modifier) => {
        if (!modifier) return "";
        return `${prefixClass}--${modifier}`;
      })
      .filter(Boolean);
  } else if (isObject(modifierObject)) {
    modifiers = Object.entries(modifierObject).map(([modifier, value]) => {
      if (!value) return "";
      return `${prefixClass}--${modifier}`;
    });
  }
  return modifiers;
};

/**
 * CSS BEM
 * @example
 * const bem = createCssScope('button')
 * bem() // button
 * bem('label') // button__label
 * bem({ disabled }) // button button--disabled
 * bem('label', { disabled }) // button__label button__label--disabled
 * bem(['disabled', 'primary']) // button button--disabled button--primary
 * bem([type, status, shape, size], {loading: loading,long: long,disabled: disabled}),
 * bem('main',[type, status, shape, size], {loading: loading,long: long,disabled: disabled}),
 */

export const createCssScope = (prefix: string, identity = "front") => {
  const prefixClass = `${identity}-${prefix.replace(identity, "")}`;

  return (
    elementOrModifier?: BEMElement | BEMModifier,
    modifier?: BEMModifier,
    modifierLater?: BEMModifier
  ) => {
    if (!elementOrModifier) return prefixClass;
    if (isString(elementOrModifier)) {
      const element = `${prefixClass}__${elementOrModifier}`;
      if (!modifier) return element;
      return [
        element,
        ...createModifier(element, modifier),
        ...createModifier(element, modifierLater),
      ];
    }
    return [
      prefixClass,
      ...createModifier(prefixClass, elementOrModifier),
      ...createModifier(prefixClass, modifier),
    ];
  };
};

七、在项目使用BEM

import { createCssScope } from '../../utils/bem'
const bem = createCssScope('button')
<button
    :class="[
      bem([type, status, shape, size], {
        loading: loading,
        long: long,
        disabled: disabled,
      }),
    ]"
  >
      ...
  </button>

BEM 的使用可以总结如下:

  • -(中划线):作为连接字符使用,表示某个块或子元素的多个单词之间的连接符号
  • __(双下划线):用来链接块与块的子元素
  • --(双中线):用来链接块元素与修饰符
  • 根元素用 bem()定义块
  • 用 bem('element') 定义子元素
  • 多种状态的修饰器用列表 bem([])
  • 状态为布尔类型的修饰器用对象 bem({})
  • 一个节点只用一个 bem()