Vue 组件开发中 CSS 的 BEM 规范完全指南

介绍

什么是 BEM?

什么是 BEM?

BEM 是一种 CSS 命名规范,全称为 Block(块)Element(元素)Modifier(修饰符)。这是一种可以帮助我们更好地组织和管理 CSS 代码的方法,特别适合在大型项目或团队协作中使用。

想象一下,你正在打造一座城市(你的网站):

  • Block(块) 就像是城市中的一栋独立建筑,例如商场、医院、学校。
  • Element(元素) 是这栋建筑内部的组成部分,如商场里的商店、电梯、走廊。
  • Modifier(修饰符) 则是描述这些建筑或内部组件的特定状态或变体,比如一个"豪华版"的商场或"紧急状态"的电梯。

为什么要使用 BEM?

也许你会问:"我一直用普通的 CSS 类名也没问题,为什么要学习新的命名规范呢?"

这里有几个让你心动的理由:

  1. 减少样式冲突:BEM 提供了一种命名约定,可以显著降低类名冲突的概率。
  2. 提高代码可读性:通过类名就能清晰地看出元素之间的层级关系。
  3. 增强代码可维护性:当项目变大时,BEM 可以帮助你快速定位和修改样式。
  4. 利于团队协作:团队成员可以更容易理解彼此的代码,减少沟通成本。

BEM 的基本语法

BEM 命名规则看起来是这样的:

CSS
css
.block {}
.block-block {}
.block__element {}
.block--modifier {}
.block__element--modifier {}

让我们详细解释每个部分:

  1. Block(块):独立存在的组件,如 .nc-button.nc-header.nc-button-group
  2. Element(元素):块的一部分,依赖于块,如 .nc-button__icon.nc-menu__item
  3. Modifier(修饰符):改变块或元素的外观或行为,如 .nc-button--large.nc-menu__item--active

BEM 还有一个常见扩展:State(状态),通常以 is- 前缀表示,如 .is-disabled.is-active

TIP

讲到这里你应该发现了,很多流行的组件库如 Element PlusAnt Design Vue 等都在使用 BEM 命名规范。这不是巧合,而是因为 BEM 确实能够有效地解决大型项目中的样式管理问题。

BEM 在前端开发中的应用

传统 CSS 中的 BEM

在传统 CSS 中,我们通常直接写类名:

index.html
html
<button class="button button--primary button__icon--left">
  <span class="button__icon"> 图标 </span>
  <span class="button__text"> 按钮文本 </span>
</button>
index.css
css
.button {
  display: inline-block;
  padding: 10px 15px;
}

.button--primary {
  background-color: blue;
  color: white;
}

.button__icon {
  margin-right: 5px;
}

.button__icon--left {
  float: left;
}

.button__text {
  font-weight: bold;
}

虽然这种方法可行,但在大型项目中手动编写这些类名会变得繁琐和易错。别担心,接下来我们将介绍更高效的方法!

在 Vue 组件中使用 BEM

在 Vue 组件中,我们可以使用 TypeScript 和 SCSS 来简化 BEM 的使用。下面我们将探讨两种方式:

  1. 使用 TypeScript 函数生成 BEM 类名
  2. 使用 SCSS 混入创建 BEM 样式

工具函数封装

一、TypeScript 版 BEM 工具函数

NOTE

首先,让我们来看看如何使用 TypeScript 封装一个 BEM 工具函数。这个函数可以帮助我们在 Vue 组件的模板中轻松生成符合 BEM 规范的类名。

我们将分步骤讲解这个工具函数的实现:

步骤 1:定义 BEM 接口

先创建一个create.ts文件

首先,我们需要定义清晰的接口,指定我们的 BEM 函数应该具有什么功能:

create.ts
typescript
// 定义 BEM 函数接口
interface BEM {
  // 主函数:可以同时指定块、元素和修饰符
  (block?: string, element?: string, modifier?: string): string

  // 单独的方法
  b: (block?: string) => string // 仅块
  e: (element: string) => string // 仅元素
  m: (modifier: string) => string // 仅修饰符
  be: (block: string, element: string) => string // 块+元素
  bm: (block: string, modifier: string) => string // 块+修饰符
  em: (element: string, modifier: string) => string // 元素+修饰符
  is: (name: string, state: string | boolean) => string // 状态
}

这个接口定义了我们的 BEM 函数的所有功能:

  • 它可以作为函数直接调用,同时指定块、元素和修饰符
  • 它也有多个单独的方法,用于不同的 BEM 组合情况

步骤 2:创建基础类名生成函数

NOTE

接下来,我们需要一个基础函数来生成 BEM 类名:

create.ts
typescript
/**
 * 生成 BEM 规范的类名
 * @private
 * @param prefixName - 前缀名称
 * @param block - 块名称
 * @param element - 元素名称
 * @param modifier - 修饰符名称
 * @returns 生成的类名
 */
const generateBEM = (prefixName: string, block?: string, element?: string, modifier?: string): string => {
  let className = prefixName

  if (block) {
    className += `-${block}`
  }

  if (element) {
    className += `__${element}`
  }

  if (modifier) {
    className += `--${modifier}`
  }

  return className
}

这个函数很简单,它接收前缀、块、元素和修饰符作为参数,并将它们按照 BEM 规范组合成类名。

步骤 3:创建 BEM 函数生成器

NOTE

现在,我们创建一个函数,它会返回符合我们定义的 BEM 接口的函数:

create.ts
typescript
/**
 * 创建 BEM 命名生成器
 * @private
 * @param prefixName - 前缀名称
 * @returns BEM 命名生成函数及其方法集合
 */
const createBEM = (prefixName: string): BEM => {
  // 基础方法
  const b = (block?: string): string => generateBEM(prefixName, block)

  const e = (element: string): string => (element ? generateBEM(prefixName, '', element) : '')

  const m = (modifier: string): string => (modifier ? generateBEM(prefixName, '', '', modifier) : '')

  // 组合方法
  const be = (block: string, element: string): string => generateBEM(prefixName, block, element)

  const bm = (block: string, modifier: string): string => generateBEM(prefixName, block, '', modifier)

  const em = (element: string, modifier: string): string =>
    !element && !modifier ? '' : generateBEM(prefixName, '', element, modifier)

  // 状态方法
  const is = (name: string, state: string | boolean): string => (state ? `is-${name}` : '')

  // 主函数
  const bem = ((block?: string, element?: string, modifier?: string): string =>
    generateBEM(prefixName, block, element, modifier)) as BEM

  // 扩展方法
  Object.assign(bem, {
    b,
    e,
    m,
    be,
    bm,
    em,
    is
  })

  return bem
}

这个函数创建了所有我们需要的 BEM 方法,并将它们挂载到主函数上,使其符合我们定义的 BEM 接口。

步骤 4:创建命名空间函数

NOTE

最后,我们创建一个公共函数,用于创建特定组件的 BEM 命名空间:

create.ts
typescript
/**
 * 创建命名空间
 * @public
 * @param name - 组件名称
 * @param prefix - 组件前缀
 *
 */
export const createNamespace = (name: string, prefix: string = 'nc'): BEM => { 
  const prefixName = `${prefix}-${name}`
  return createBEM(prefixName)
}

这个函数允许我们为每个组件创建一个独立的 BEM 命名空间,使用统一的前缀(这里我默认为 nc,你也可以设置为任意的默认值,比如el)。

步骤 5:在 Vue 单文件组件中使用

NOTE

现在我们已经完成了 BEM 工具函数的封装,让我们看看如何在实际的 Vue 单文件组件中使用它。

首先,创建一个简单的 Card 组件:

Card.vue
vue
<template>
  <!-- 基本用法1:使用 bem.b() 生成块类名 -->
  <div :class="bem.b()">
    <!-- 基本用法2:使用 bem.e() 生成元素类名 -->
    <div :class="bem.e('header')">
      <h3 :class="bem.e('title')">{{ title }}</h3>
      <!-- 基本用法3:使用 bem.is() 生成状态类名 -->
      <span :class="bem.is('closable', closable)" v-if="closable" @click="emit('close')">×</span>
    </div>

    <!-- 基本用法4:使用 bem.e() 和 bem.m() 组合 -->
    <div
      :class="[
        bem.e('body'),
        bem.m(type) // 添加修饰符类名
      ]"
    >
      <slot></slot>
    </div>

    <!-- 基本用法5:在条件渲染中使用 -->
    <div v-if="footer" :class="bem.e('footer')">
      {{ footer }}
    </div>
  </div>
</template>

<script setup lang="ts">
import { createNamespace } from '@/utils/create'

interface Props {
  title?: string
  type?: string
  closable?: boolean
  footer?: string
}

const props = withDefaults(defineProps<Props>(), {
  title: '标题',
  type: 'default',
  closable: false,
  footer: ''
})

const emit = defineEmits<{
  (e: 'close'): void
}>()

// 创建BEM命名空间,参数是组件名
const bem = createNamespace('card')
</script>

<style lang="scss">
// 不使用SCSS混入的普通样式写法(有更方便的方式,下面会继续讲解) 
.nc-card {
  border: 1px solid #eee;
  border-radius: 4px;
  overflow: hidden;
  margin-bottom: 20px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);

  &__header {
    padding: 15px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    border-bottom: 1px solid #eee;
  }

  &__title {
    margin: 0;
    font-size: 16px;
    font-weight: bold;
  }

  &__body {
    padding: 20px;
  }

  // 不同类型的卡片
  &--primary {
    border-color: #1890ff;
    .nc-card__header {
      background-color: #1890ff;
      color: white;
    }
  }

  &--success {
    border-color: #52c41a;
    .nc-card__header {
      background-color: #52c41a;
      color: white;
    }
  }

  &__footer {
    padding: 15px;
    border-top: 1px solid #eee;
    background-color: #f7f9fa;
  }

  // 状态类
  &.is-closable {
    .nc-card__header span {
      cursor: pointer;
      font-size: 20px;
      &:hover {
        color: #f56c6c;
      }
    }
  }
}
</style>

解释说明:

  1. 初始化 BEM 命名空间

    ts
    const bem = createNamespace('card')

    这一步,创建了一个名为 card 的 BEM 命名空间。

  2. 生成块类名

    vue
    <div :class="bem.b()"></div>

    这会生成 .nc-card 类名。

    vue
    <div :class="bem.b('box')"></div>

    这会生成 .nc-card-box 类名。

  3. 生成元素类名

    vue
    <div :class="bem.e('header')"></div>

    这会生成 .nc-card__header 类名。

  4. 生成带修饰符的类名

    vue
    <div :class="[bem.e('body'), bem.m(type)]"></div>

    type="success" 时,会生成 .nc-card__body.nc-card--success 类名。

  5. 生成状态类名

    vue
    <span :class="bem.is('closable', closable)"></span>

    closable=true 时,会生成 .is-closable 类名。

通过这种方式,我们可以在 Vue 组件中轻松管理 BEM 类名,同时保持代码的清晰和可维护性。当组件名称或样式前缀需要更改时,我们只需修改一处即可(createNamespace 的参数),而不需要在模板中搜索并替换所有类名。

二、SCSS 版 BEM 混入

NOTE

除了使用 TypeScript 生成类名外,我们还可以使用 SCSS 混入来简化 BEM 样式的编写。SCSS 混入允许我们在样式表中以更加结构化和语义化的方式定义 BEM 样式。

首先,我们需要创建一些配置变量:

步骤 1:创建 SCSS 配置文件

首先创建config.scss存放变量名称

config.scss
scss
// config.scss

// 命名空间,用于所有组件类名的前缀
// 例如:.nc-button, .nc-card 等
$namespace: 'nc' !default; 

// 块与命名空间之间的连接符
// 例如:nc-button 中的 '-'
$common-separator: '-' !default; 

// 元素分隔符,用于连接块和元素
// 例如:.nc-button__icon 中的 '__'
$element-separator: '__' !default; 

// 修饰符分隔符,用于连接块/元素和修饰符
// 例如:.nc-button--primary 中的 '--'
$modifier-separator: '--' !default; 

// 状态前缀,用于表示组件的不同状态
// 例如:.is-disabled, .is-active 等
$state-prefix: 'is-' !default; 

这些变量定义了 BEM 命名规范中使用的前缀和分隔符。通过修改这些变量,你可以定制自己的命名风格,比如将命名空间从 nc 改为 el(Element UI 使用的前缀)或 a(Ant Design 风格)。

!default 标记表示这些是默认值,你可以在导入配置文件之前重新定义这些变量来覆盖默认值,非常适合主题定制。

步骤 2:创建基础的 BEM 混入

创建mixin.scss,首先引入config.scss然后继续定义 mixin 混入

mixin.scss
scss
@use 'config' as *;
@forward 'config';

我们先从最简单的 block 混入开始:

mixin.scss
scss
// 创建 BEM 块级选择器
// $block - 块名称
@mixin b($block) {
  $B: $namespace + $common-separator + $block !global;
  .#{$B} {
    @content;
  }
}

这个混入可以帮助我们创建块级选择器,并将块名保存到一个全局变量中,以便其他混入使用。

用法示例:

scss
@include b(button) {
  color: red;
  font-size: 14px;
}
css
.nc-button {
  color: red;
  font-size: 14px;
}

步骤 3:创建元素混入

接下来,我们创建用于定义元素的混入:

mixin.scss
scss
// 创建 BEM 元素选择器
// $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;
      }
    }
  }
}

这个混入可以在块内部创建元素选择器,并处理特殊嵌套规则。

用法示例:

scss
@include b(button) {
  // 定义块样式
  background: #fff;

  // 定义元素样式
  @include e(icon) {
    margin-right: 5px;
  }
}
css
.nc-button {
  background: #fff;
}
.nc-button__icon {
  margin-right: 5px;
}

步骤 4:创建修饰符混入

然后,我们创建用于定义修饰符的混入:

mixin.scss
scss
// 创建 BEM 修饰符选择器
// $modifier - 修饰符名称
@mixin m($modifier) {
  $selector: &;
  $currentSelector: '';
  @each $unit in $modifier {
    $currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ','};
  }

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

这个混入可以在块或元素内部创建修饰符选择器。

用法示例:

scss
@include b(button) {
  color: #333;

  // 定义修饰符样式
  @include m(primary) {
    color: white;
    background: blue;
  }

  @include m(danger) {
    color: white;
    background: red;
  }
}
css
.nc-button {
  color: #333;
}
.nc-button--primary {
  color: white;
  background: blue;
}
.nc-button--danger {
  color: white;
  background: red;
}

步骤 5:创建状态混入

我们还需要创建一个用于定义状态的混入:

mixin.scss
scss
// 创建状态类选择器
// $state - 状态名称
@mixin when($state) {
  @at-root {
    &.#{$state-prefix + $state} {
      @content;
    }
  }
}

这个混入可以创建状态选择器,用于定义元素的不同状态。

用法示例:

scss
@include b(button) {
  cursor: pointer;

  // 定义状态样式
  @include when(disabled) {
    cursor: not-allowed;
    opacity: 0.5;
  }

  @include when(active) {
    border-color: blue;
  }
}
css
.nc-button {
  cursor: pointer;
}
.nc-button.is-disabled {
  cursor: not-allowed;
  opacity: 0.5;
}
.nc-button.is-active {
  border-color: blue;
}

步骤 5.5:创建伪类选择器混入

我们再来创建一个用于定义伪类的混入,它可以方便地为元素添加伪类样式:

mixin.scss
scss
// 创建伪类选择器
// $pseudo - 伪类名称
@mixin pseudo($pseudo) {
  @at-root {
    &:#{$pseudo} {
      @content;
    }
  }
}

这个混入可以创建伪类选择器,用于定义元素的不同状态的样式,如:hover:focus:active等。

用法示例:

scss
@include b(button) {
  color: blue;
  
  // 定义伪类样式
  @include pseudo(hover) {
    color: darkblue;
    text-decoration: underline;
  }
  
  @include pseudo(focus) {
    outline: 2px solid blue;
  }
  
  @include e(icon) {
    margin-right: 5px;
    
    // 元素的伪类
    @include pseudo(hover) {
      transform: scale(1.2);
    }
  }
}
css
.nc-button {
  color: blue;
}
.nc-button:hover {
  color: darkblue;
  text-decoration: underline;
}
.nc-button:focus {
  outline: 2px solid blue;
}
.nc-button__icon {
  margin-right: 5px;
}
.nc-button__icon:hover {
  transform: scale(1.2);
}

步骤 6:创建辅助函数

最后,我们需要一些辅助函数来支持我们的混入:

mixin.scss
scss
// 检查是否命中所有特殊嵌套规则
@function hitAllSpecialNestRule($selector) {
  @return containsModifier($selector) or containWhenFlag($selector) or containPseudoClass($selector);
}

// 检查选择器是否包含修饰符
@function containsModifier($selector) {
  $selector: selectorToString($selector);
  @return str-index($selector, $modifier-separator);
}

// 检查选择器是否包含状态类
@function containWhenFlag($selector) {
  $selector: selectorToString($selector);
  @return str-index($selector, '.' + $state-prefix);
}

// 检查选择器是否包含伪类
@function containPseudoClass($selector) {
  $selector: selectorToString($selector);
  @return str-index($selector, ':');
}

// 将选择器转换为字符串
@function selectorToString($selector) {
  $selector: inspect($selector);
  $selector: str-slice($selector, 2, -2);
  @return $selector;
}

这些辅助函数主要在内部使用,不需要直接调用。它们帮助混入处理嵌套规则,比如:

scss
@include b(button) {
  @include m(primary) {
    background: blue;

    // 在修饰符内部使用元素
    @include e(icon) {
      // 这里会被正确处理为:.nc-button--primary .nc-button__icon
      color: white;
    }
  }
}
css
.nc-button--primary {
  background: blue;
}
.nc-button--primary .nc-button__icon {
  color: white;
}

实战应用

TIP

现在,我们已经了解了两种 BEM 实现方式。让我们看看如何在实际的 Vue 组件中使用它们。

结合两种方式的最佳实践

实际上,我们可以结合这两种方式,获得最佳的开发体验:

示例 :结合 TS 和 SCSS 的按钮组件

Button.vue
vue
<template>
  <button :class="[bem.b(), bem.m(type), bem.is('disabled', disabled)]">
    <span v-if="icon" :class="bem.e('icon')">
      <i :class="icon"></i>
    </span>
    <span :class="bem.e('text')">
      <slot></slot>
    </span>
  </button>
</template>

<script setup lang="ts">
import { createNamespace } from '@/utils/create'

interface Props {
  type?: string
  icon?: string
  disabled?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  type: 'default',
  icon: '',
  disabled: false
})

// 创建 BEM 命名空间
const bem = createNamespace('button')
</script>

<style lang="scss">
@use '@/theme-chalk/src/mixin' as *;

@include b(button) {
  display: inline-block;
  padding: 10px 15px;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-size: 14px;
  cursor: pointer;

  @include m(primary) {
    background-color: #1890ff;
    color: white;
    border-color: #1890ff;
  }

  @include m(success) {
    background-color: #52c41a;
    color: white;
    border-color: #52c41a;
  }

  @include e(icon) {
    margin-right: 5px;
  }

  @include e(text) {
    font-weight: 500;
  }

  @include when(disabled) {
    opacity: 0.5;
    cursor: not-allowed;
  }
}
</style>

在这个例子中:

  • 我们在模板中使用 TS 版 BEM 工具生成类名,这使得类名的生成更加灵活和动态
  • 我们在样式中使用 SCSS 混入定义样式,这使得样式代码更加结构化和可读

这种结合方式让我们能够充分利用两种工具的优势,实现最佳的开发体验。

完整代码

IMPORTANT

为方便读者直接使用,以下是本教程中涉及的三个关键文件的完整代码:

create.ts

create.ts
typescript
/**
 * BEM 命名规范工具
 *
 * @description
 * Block:独立的组件块 (.block)
 * Element:组件的子元素 (.block__element)
 * Modifier:组件或子元素的变体 (.block--modifier, .block__element--modifier)
 * State:组件的状态 (.is-state)
 *
 */

// BEM接口定义
interface BEM {
  (block?: string, element?: string, modifier?: string): string
  b: (block?: string) => string
  e: (element: string) => string
  m: (modifier: string) => string
  be: (block: string, element: string) => string
  bm: (block: string, modifier: string) => string
  em: (element: string, modifier: string) => string
  is: (name: string, state: string | boolean) => string
}

/**
 * 生成BEM规范的类名
 * @private
 * @param prefixName - 前缀名称
 * @param block - 块名称
 * @param element - 元素名称
 * @param modifier - 修饰符名称
 * @returns 生成的类名
 */
const generateBEM = (prefixName: string, block?: string, element?: string, modifier?: string): string => {
  let className = prefixName

  if (block) {
    className += `-${block}`
  }

  if (element) {
    className += `__${element}`
  }

  if (modifier) {
    className += `--${modifier}`
  }

  return className
}

/**
 * 创建BEM命名生成器
 * @private
 * @param prefixName - 前缀名称
 * @returns BEM命名生成函数及其方法集合
 */
const createBEM = (prefixName: string): BEM => {
  // 基础方法
  const b = (block?: string): string => generateBEM(prefixName, block)

  const e = (element: string): string => (element ? generateBEM(prefixName, '', element) : '')

  const m = (modifier: string): string => (modifier ? generateBEM(prefixName, '', '', modifier) : '')

  // 组合方法
  const be = (block: string, element: string): string => generateBEM(prefixName, block, element)

  const bm = (block: string, modifier: string): string => generateBEM(prefixName, block, '', modifier)

  const em = (element: string, modifier: string): string =>
    !element && !modifier ? '' : generateBEM(prefixName, '', element, modifier)

  // 状态方法
  const is = (name: string, state: string | boolean): string => (state ? `is-${name}` : '')

  // 主函数
  const bem = ((block?: string, element?: string, modifier?: string): string =>
    generateBEM(prefixName, block, element, modifier)) as BEM

  // 扩展方法
  Object.assign(bem, {
    b,
    e,
    m,
    be,
    bm,
    em,
    is
  })

  return bem
}

/**
 * 创建命名空间
 * @public
 * @param name - 组件名称
 * @param prefix - 组件前缀,默认为 'nc'
 * @returns BEM命名生成器
 */
export const createNamespace = (name: string, prefix: string = 'nc'): BEM => {
  const prefixName = `${prefix}-${name}`
  return createBEM(prefixName)
}

// 类型导出
export type { BEM }

config.scss

config.scss
scss
$namespace: 'nc' !default;

$common-separator: '-' !default;

$element-separator: '__' !default;

$modifier-separator: '--' !default;

$state-prefix: 'is-' !default;

mixin.scss

mixin.scss
scss
@use 'config' as *;
@forward 'config';

// 创建 BEM 块级选择器
// $block - 块名称
@mixin b($block) {
  $B: $namespace + $common-separator + $block !global;
  .#{$B} {
    @content;
  }
}

// 创建 BEM 元素选择器
// $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;
      }
    }
  }
}

// 创建 BEM 修饰符选择器
// $modifier - 修饰符名称
@mixin m($modifier) {
  $selector: &;
  $currentSelector: '';
  @each $unit in $modifier {
    $currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ','};
  }

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

// 创建状态类选择器
// $state - 状态名称
@mixin when($state) {
  @at-root {
    &.#{$state-prefix + $state} {
      @content;
    }
  }
}

// 创建伪类选择器
// $pseudo - 伪类名称
@mixin pseudo($pseudo) {
  @at-root {
    &:#{$pseudo} {
      @content;
    }
  }
}

// 检查是否命中所有特殊嵌套规则
@function hitAllSpecialNestRule($selector) {
  @return containsModifier($selector) or containWhenFlag($selector) or containPseudoClass($selector);
}

// 检查选择器是否包含修饰符
@function containsModifier($selector) {
  $selector: selectorToString($selector);
  @return str-index($selector, $modifier-separator);
}

// 检查选择器是否包含状态类
@function containWhenFlag($selector) {
  $selector: selectorToString($selector);
  @return str-index($selector, '.' + $state-prefix);
}

// 检查选择器是否包含伪类
@function containPseudoClass($selector) {
  $selector: selectorToString($selector);
  @return str-index($selector, ':');
}

// 将选择器转换为字符串
@function selectorToString($selector) {
  $selector: inspect($selector);
  $selector: str-slice($selector, 2, -2);
  @return $selector;
}

常见问题与解答

1. 为什么我的 SCSS 混入没有正确生成选择器?

可能的原因:

  • 确保你正确导入了 mixin.scss 文件
  • 检查是否正确使用了混入语法,例如 @include b(button)
  • 确保 config.scss 中的配置变量与你的预期一致

2. TS 版 BEM 工具生成的类名与我手动写的不一样?

可能的原因:

  • 检查传入的参数是否正确
  • 确保你正确导入和使用了 createNamespace 函数
  • 确保默认前缀与你的预期一致

3. 我应该选择哪种 BEM 实现方式?

这取决于你的具体需求:

  • 如果你需要动态生成类名,或者需要在 JavaScript 中操作类名,选择 TS 版 BEM 工具
  • 如果你主要关注样式的组织和可读性,选择 SCSS 混入
  • 如果你想要最佳的开发体验,可以结合两种方式使用

4. BEM 命名是否会导致类名过长?

是的,BEM 命名确实会导致类名变长,但这是为了提高可读性和避免冲突的权衡。在实际项目中,类名的长度对性能影响很小,而良好的命名规范带来的可维护性收益远大于这一点小小的代价。

最后,希望这篇教程对你有所帮助,祝你的前端开发之旅愉快!