BEM 规范及实战快速生成

这篇文章我将基于Yike-Design组件库的开发具体讲述BEM规范

在此基础上我将介绍在实际开发过程中运用的命名空间方法快速生成符合BEM规范的类名

1. 什么是BEM

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

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

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

2. 为什么我们要使用BEM

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

bem这套规范其实非常符合"组件化"、"模块化"的直觉,用"块"维护一个组件,用"块儿"中的元素维护一个独立的组成部分,这种具备层级的结构其实也是我开发过程中经常采用的思路,后续在开发过程中也应当采用此规范,并根据我们的公共方法进行快速的类名定义

3. 实例

理论说完了,让我们用一个实例来具体说说我在开发时的具体的结构设计和思路,这边我挑一个并不是我写的组件Checkbox尝试一下~

Step1. 确定组件结构和"块"的划分

显然,从组件设计上讲,我们会倾向于用checkbox-group.vuecheckbox(-item).vue这两个组件来分别维护多选组和多选选项,那么我们在编码时,便确定了yk-checkboxyk-checkbox-group这两个顶层"块儿"

  • checkbox.vue
html 复制代码
<div class="yk-checkbox">
    ...
</div>
  • checkbox-group.vue
html 复制代码
<div class="yk-checkbox-group">
    ...
</div>

Step2. 区分元素

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

块儿与元素应当用 __ 进行链接,并使用小写字母和短横线进行命名

例如 yk-checkbox__box-container

  • checkbox.vue
html 复制代码
<div class="yk-checkbox">
    <div class="yk-checkbox__box">
        <div class="yk-checkbox__icon"></div>
    </div>
    <div class="yk-checkbox__label">
        {{ name }}
    </div>
</div>

Step3. 确定修饰器

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

从设计图上来看, box 这个元素具有禁用(disabled)选中状态(status)这两个修饰

其中,disabled的取值是 truefalse 我们通常的做法是取值为true的时候为他加上这个修饰

而status的状态有三种,分别是未选(normal)选中(active),半选(indeterminate)

修饰器采用--与元素和块直接链接

我们用两个实例来看一下具体的类名应该是啥样

  • checkbox.vue
html 复制代码
<div class="yk-checkbox">
    <div class="yk-checkbox__box yk-checkbox__box--normal">
        <div class="yk-checkbox__icon"></div>
    </div>
    <div class="yk-checkbox__label">
        {{ name }}
    </div>
</div>
  • checkbox.vue
html 复制代码
<div class="yk-checkbox">
    <div class="yk-checkbox__box yk-checkbox__box--active yk-checkbox__box--disabled">
        <div class="yk-checkbox__icon"></div>
    </div>
    <div class="yk-checkbox__label">
        {{ name }}
    </div>
</div>

4. 快速生成BEM

当然,在项目的实战过程中,实现这套命名规范的方式有很多,常规的可能是采用计算属性为class赋值,比如我们最初的yk-button为了实现根据props传入的内容区分样式,我们曾经的代码是

javascript 复制代码
const ykButtonClass = computed(() => {
  return {
    'yk-button': true,
    'yk-button--loading': props.loading,
    'yk-button--long': props.long,
    'yk-button--disabled': props.disabled || props.loading,
    [`yk-button--${props.status}`]: props.status,
    [`yk-button--${props.type}`]: props.type,
    [`yk-button--${props.size}`]: props.size,
    [`yk-button--${props.shape}`]: props.shape,
  }
})

而采用命名方法后,类名的定义可以这样去实现

html 复制代码
  <button
    :class="[
      bem([type, status, shape, size], {
        loading: loading,
        long: long,
        disabled: disabled,
      }),
    ]"
  >
      ...
  </button>

个人认为这套方法在开发效率上还是有一定的优势的,我们在后文中继续介绍它的使用方法和源码解析

5. 使用方法

引入并定义块

javascript 复制代码
import { createCssScope } from '../../utils/bem'
const bem = createCssScope('button')

后续,我们可以在模板中采用bem()方法快速地定义附带前缀的各个元素和修饰器

定义元素

还记得我们前文的例子么,现在类名的定义可以这样去实现了

html 复制代码
<div :class="bem()">
    <div :class="bem('box',[status],{disabled})">
        <div :class="bem('icon')"></div>
    </div>
    <div :class="icon('label')">
        {{ name }}
    </div>
</div>

const disabled = true;
const status = 'active'

可以总结为以下几句话

  • 根元素用bem()定义块
  • bem('element')定义子元素
  • 多种状态的修饰器用列表 bem([])
  • 状态为布尔类型的修饰器用对象 bem({})
  • 一个节点只用一个bem()

6. 源码分析

源码我先干上来

javascript 复制代码
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;
};

export const createCssScope = (prefix: string, identity = 'yk') => {
  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),
    ];
  };
};
  • 首先是初始化这块儿,拿到传入的组件名拼上yk-前缀作为我们的顶层块,后面采用返回的函数生成的类名都会拼上这个块的前缀
javascript 复制代码
export const createCssScope = (prefix: string, identity = 'yk') => {
  const prefixClass = `${identity}-${prefix.replace(identity, '')}`;
};
  • 返回的函数提供了三个入参,这边是为了区分针对的修饰和针对元素的修饰,若首个入参为字符串,则此函数用于元素,采用后面两个入参作为修饰器,返回函数的出参均为一个类名的列表,为了避免多个修饰器存在重复元素,我们可以通过解构过滤掉
javascript 复制代码
if (!elementOrModifier) return prefixClass;
    if (isString(elementOrModifier)) {
      ...
      // 元素修饰
      return [
        element,
        ...createModifier(element, modifier),
        ...createModifier(element, modifierLater),
      ];
    }
    // 块修饰
    return [
      prefixClass,
      ...createModifier(prefixClass, elementOrModifier),
      ...createModifier(prefixClass, modifier),
    ];
  • createModifier这边有两个入参,即元素前缀和修饰器对象,最终返回的都是一个列表,列表的每个内容即一个修饰器对应的类名
javascript 复制代码
const createModifier = (prefixClass: string, modifierObject?: BEMModifier) => {
  let modifiers: string[] = [];
   modifiers = obj.map()=>{
       return `${prefixClass}--${modifier}`;
   }
  return modifiers;
};
  • 当然,这边我们区分了两种传入的方式,如上文,列表为多状态修饰,对象为单状态修饰,其中的filter和逻辑!部分的代码是为了过滤掉键值为undefined的修饰
javascript 复制代码
 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}`;
    });
  }

7. 总结

综上,Yike-Design组件库中关于bem的介绍和实现方法到这里就结束了

有建议和指导欢迎随时指出~

相关推荐
m0_748247552 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php
m0_748255022 小时前
前端常用算法集合
前端·算法
真的很上进3 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
web130933203983 小时前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
NiNg_1_2343 小时前
Echarts连接数据库,实时绘制图表详解
前端·数据库·echarts
如若1234 小时前
对文件内的文件名生成目录,方便查阅
java·前端·python
滚雪球~5 小时前
npm error code ETIMEDOUT
前端·npm·node.js
沙漏无语5 小时前
npm : 无法加载文件 D:\Nodejs\node_global\npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
supermapsupport5 小时前
iClient3D for Cesium在Vue中快速实现场景卷帘
前端·vue.js·3d·cesium·supermap
brrdg_sefg5 小时前
WEB 漏洞 - 文件包含漏洞深度解析
前端·网络·安全