一、什么是 BEM
BEM 是由 Yandex 公司推出的一套 CSS 命名规范,官方是这么描述它的:BEM 是一种让你可以快速开发网站并对此进行多年维护的技术。
BEM(Block, Element, Modifier)是一种前端编码规范,用于命名 HTML 和 CSS 中的类和选择器,它旨在提供一种一致的方式来组织和命名代码,使其易于理解、扩展和维护。
以下是 BEM 规范的基本原则:
- 块(Block):块是一个独立的可重用组件,它代表一个完整的实体,它是整个 BEM 结构中最高层级的部分,应该有一个唯一的类名。 示例:.el-button、.el-navbar
- 元素(Element):元素是块的组成部分,不能独立存在。它们依赖于块的上下文,并且有属于块的类名作为前缀。 示例:.button__text、.navbar__item
- 修饰符(Modifier):修饰符用于修改块或元素的外观、状态或行为。它们是可选的,可以单独使用或与块或元素的类名结合使用。 示例:.el-button--large、.el-upload__item--active
二、BEM命名的好处
BEM命名的核心就是可以清晰的描述页面的结构,从其名字就可以知道某个标记的含义,于是通过查看 class 属性就可以知道元素之间的关联,具体的好处如下:
- 提供一种一致的命名约定,使团队可以更轻松地理解和维护代码
- 促进可重用性和模块化开发
- 减少 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组件)来具体说说我在开发时的具体的结构设计和思路:
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)
<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>
<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()