这篇文章我将基于Yike-Design组件库的开发具体讲述BEM规范
在此基础上我将介绍在实际开发过程中运用的命名空间方法
以快速生成
符合BEM规范的类名
1. 什么是BEM
BEM(Block, Element, Modifier)是一种前端编码规范,用于命名 HTML 和 CSS 中的类和选择器。它旨在提供一种一致的方式来组织和命名代码,使其易于理解、扩展和维护。
以下是 BEM 规范的基本原则:
- 块(Block) :块是一个独立的可重用组件,它代表一个完整的实体。它是整个 BEM 结构中最高层级的部分,应该有一个
唯一
的类名。 示例:.yk-button
、.yk-navbar
- 元素(Element) :元素是块的
组成部分
,不能独立存在。它们依赖于块的上下文,并且有属于块的类名作为前缀。 示例:.button__text
、.navbar__item
- 修饰符(Modifier) :修饰符用于修改块或元素的外观、状态或行为。它们是可选的,可以单独使用或与块或元素的类名结合使用。 示例:
.yk-button--large
、.yk-upload__item--active
2. 为什么我们要使用BEM
- 提供一种一致的命名约定,使团队可以更轻松地理解和维护代码
- 促进可重用性和模块化开发
- 减少 CSS 的特异性(specificity)问题,避免组件间样式冲突
bem这套规范其实非常符合"组件化"、"模块化"的直觉,用"块"维护一个组件,用"块儿"中的元素维护一个独立的组成部分,这种具备层级
的结构其实也是我开发过程中经常采用的思路,后续在开发过程中也应当采用此规范,并根据我们的公共方法进行快速的类名定义
3. 实例
理论说完了,让我们用一个实例来具体说说我在开发时的具体的结构设计和思路,这边我挑一个并不是我写的组件Checkbox
尝试一下~
Step1. 确定组件结构和"块"的划分
显然,从组件设计上讲,我们会倾向于用checkbox-group.vue
和checkbox(-item).vue
这两个组件来分别维护多选组和多选选项,那么我们在编码时,便确定了yk-checkbox
和yk-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的取值是 true
和false
我们通常的做法是取值为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的介绍和实现方法到这里就结束了
有建议和指导欢迎随时指出~