BEM其实是块(block),元素(element),修饰符(modifier)的缩写,利用不同的区块,功能以及样式来给元素命名。其目的是将用户界面分割成独立的区块,使得即使有复杂的UI,界面开发也能变得更加简单和快速;而且能使代码变得低耦合和高复用。常用的命名约定模式如下:
.block-name {}
.block-name__element-name {}
.block-name--modifier-name {}
/* 也可以用简短一点的命名方式 */
.blockName {}
.blockName_elementName {}
.blockName-modifierName {}
block
代表了更高级别的抽象或组件
block__element
代表 block 的后代,用于形成一个完整的 block 的整体
block--modifier
(block__element--modifier
)用来限定修饰block(element)的样式,状态或行为
BEM的关键是光凭class名字就可以让其他开发者知道某个标记用来做什么的,明白各个模块之间的关系,例如如下的命名:
.nav{}
.nav__item{}
.nav--blue{}
这几个class名很明显能看出各个块的作用,顶级块是nav,它有一些元素比如item,item又有一些属性,例如blue,但是如果写成常规的css就会很难看得出它们的关系:
.nav{}
.item{}
.blue{}
虽然看每一个class名知道它们代表什么,但是却看不出它们之间的关系,这样对比,很明显能看出BEM命名的优势。
block代表了功能上独立的可复用的组件,在HTML中,block用class属性来表示
特性:
menu
或button
),而不应该描述他的状态和外观(如red
或big
)
<div class="menu">div>
<div class="red-text">div>
.menu
)来表示block,而不能使用标签(如div
)或者id(如#myButton
)
<header class="header">
<div class="logo">div>
<div class="search">div>
header>
一个元素是块的一部分,具有某种功能。元素是依赖上下文的:它们只有处于他们应该属于的块的上下文中时才是有意义的。
特性:
同样地,element的name应该描述它是一个什么元素(如item
或text
),而不应该描述他的状态和外观(如red
或big
)
element的全名一般表示为block-name__element-name
,element的名字跟block的名字一般用双下划线分隔(__)
<input class="search-form__input">
<button class="search-form__button">Searchbutton>
element可以有任意层级的相互嵌套
一个block下的所有element无论相互层级如何,都要摊开扁平的属于Block
所以 BEM 最多只有 B+E+M 三级,不可能出现 B+E+E+…+E+M 超长class名,也要求E不能同名:
<div class="search-form">
<div class="search-form__content">
<input class="search-form__input">
<button class="search-form__button">Searchbutton>
div>
div>
<div class="search-form">
<div class="search-form__content">
<input class="search-form__content__input">
<button class="search-form__content__button">Searchbutton>
div>
div>
虽然一个block可以有任意层级的element:
<div class="block">
<div class="block__elem1">
<div class="block__elem2">
<div class="block__elem3">div>
div>
div>
div>
但是在css中,应该将他们写成平级的形式:
/* Correct */
.block {}
.block__elem1 {}
.block__elem2 {}
.block__elem3 {}
/* Incorrect */
.block .block__elem1 {}
.block .block__elem1 .block__elem2 {}
这里,block相当于一个命名空间,保证了element是属于block的
<div class="search-form">
<input class="input">
<button class="button">Searchbutton>
div>
当一段代码能够被复用并且独立于其他页面组件的时候,你就应该创建一个block。当一段代码不能离开父元素(block)单独使用时,你就应该创建一个element。有一个例外,当你的element必须分成更小的subelement时,此时element的层级会比较深,为了简化结构,你可能需要创建一个服务性的block来代替创建一个element
Modifier用来定义block或element的样式,状态或者行为
特性:
modifier的name应该能够描述block或element的样式状态或者行为,如menu--focused
,menu--disabled
,el-button--primary
,el-button--warning
modifier本质上就像HTML的属性一样,同样的block因为有不同的modifier而看起来不一样。如下图,因为使用了不同的modifier,menu的样式也会不同:
modifier不能离开block或element单独使用:
<div class="search-form search-form--small">
<input class="search-form__input">
<button class="search-form__button">Searchbutton>
div>
<div class="search-form--small">
<input class="search-form__input">
<button class="search-form__button">Searchbutton>
div>
混合是一项在一个单一DOM节点上混合使用不同的BEM实体的技术
来看一个例子:
<div class="header">
<div class="search-form header__search-form">div>
div>
我们知道,我们不应该给block设置margin或改变它的定位(position),这会影响block的外界环境。在这个例子中,我们在header块中联合了search-form块和search-form元素的行为和样式,这就允许我们在header_search-form元素里面设置margin和定位,从而使search-form块保持独立性和通用性。这样,我们可以任意地方使用search-form块而不会影响它的外界环境
关于开放封闭原则,其核心的思想是软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。将这个原则应用到BEM就是,我们应该使用modifier去拓展block或element的样式,而不应该去修改block或element的基础样式,来看一个例子:
<button class="button">...button>
<button class="button">...button>
.button {
font-family: Arial, sans-serif;
text-align: center;
font-size: 18px;
line-height: 18px;
}
当我们需要改变button的size时,我们需要使用modifier,而不是直接去修改.button的样式:
<button class="button">...button>
<button class="button button--small">...button>
.button {
font-family: Arial, sans-serif;
text-align: center;
/* 不能直接修改这里 */
font-size: 18px;
line-height: 18px;
}
.button--small {
font-size: 13px;
line-height: 13px;
}
elementUI组件库就是用BEM来组织css代码的,来看一下它的部分源码:
// mixins.scss
$namespace: 'el';
$element-separator: '__';
$modifier-separator: '--';
$state-prefix: 'is-';
/* BEM
-------------------------- */
@mixin b($block) {
$B: $namespace+'-'+$block !global;
.#{$B} {
@content;
}
}
@mixin e($element) {
$E: $element !global;
$selector: &;
$currentSelector: "";
@each $unit in $element {
$currentSelector: #{$currentSelector + "." + $B + $element-separator + $unit + ","};
}
// 如果$selector里面含有一些特殊嵌套规则,比如含有modifier或伪类
@if hitAllSpecialNestRule($selector) {
@at-root {
#{$selector} {
#{$currentSelector} {
@content;
}
}
}
} @else {
@at-root {
#{$currentSelector} {
@content;
}
}
}
}
@mixin m($modifier) {
$selector: &;
$currentSelector: "";
@each $unit in $modifier {
$currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ","};
}
@at-root {
#{$currentSelector} {
@content;
}
}
}
@mixin when($state) {
@at-root {
&.#{$state-prefix + $state} {
@content;
}
}
}
elementUI中一个DOM结构比较简单的badge组件:
<template>
<div class="el-badge">
<slot>slot>
<transition name="el-zoom-in-center">
<sup
v-show="!hidden && (content || content === 0 || isDot)"
v-text="content"
class="el-badge__content"
:class="[
'el-badge__content--' + type,
{
'is-fixed': $slots.default,
'is-dot': isDot
}
]">
sup>
transition>
div>
template>
// badge.scss
// .el-badge
@include b(badge) {
position: relative;
vertical-align: middle;
display: inline-block;
// .el-badge__content
@include e(content) {
background-color: $--badge-fill;
border-radius: $--badge-radius;
color: $--color-white;
display: inline-block;
font-size: $--badge-font-size;
height: $--badge-size;
line-height: $--badge-size;
padding: 0 $--badge-padding;
text-align: center;
white-space: nowrap;
border: 1px solid $--color-white;
// .is-fixed
@include when(fixed) {
position: absolute;
top: 0;
right: #{1 + $--badge-size / 2};
transform: translateY(-50%) translateX(100%);
@include when(dot) {
right: 5px;
}
}
// .is-dot
@include when(dot) {
height: 8px;
width: 8px;
padding: 0;
right: 0;
border-radius: 50%;
}
@each $type in (primary, success, warning, info, danger) {
.el-badge--primary .el-badge--success .el-badge--warning .el-badge--info .el-badge--danger
@include m($type) {
@if $type == primary {
background-color: $--color-primary;
} @else if $type == success {
background-color: $--color-success;
} @else if $type == warning {
background-color: $--color-warning;
} @else if $type == info {
background-color: $--color-info;
} @else {
background-color: $--color-danger;
}
}
}
}
}
在element中还引入了state这个概念(如上面代码中的is-fixed, is-dot),相当于把在modifier中的状态迁移到state中。
大家有兴趣可以去看一下element的源码,可以了解到如何将BEM应用到实际的大型应用中,加深对BEM的理解
当项目中使用的是Less时,Less没有@at-root
这个特性,那么要如何实现BEM呢?一种方法是使用Parent selectors:
.block {
color: red;
&__element {
color: blue;
}
&--modifier {
color: orange;
}
}
编译出来的结果:
.block {
color: red;
}
.block__element {
color: blue;
}
.block--modifier {
color: orange;
}
如今挺多前端项目都没有一个统一的规范,前端开发人员都喜欢按照自己的套路去写代码,导致同一个项目当中有不同的命名风格,缩进风格等。虽然每个开发人员用自己舒适的套路去开发很爽,但当要去改别人的代码时,就会非常头疼了;当项目迭代到一定规模之后,就会变得很难维护了。所以一个团队有统一的规范是很重要的,本文介绍的BEM规范虽然也有缺陷(比如命名繁琐,增加了文件体积,另外编写样式,写选择器麻烦,从而增加了工作量),但你很难找到比它更好的命名规范了,从一种思想上来看,BEM还是有很多值得大家去思考的,特别是用于维护项目,或者跨团队的开发当中,它的好处会明显见长。大家不妨去尝试一下,将其运用到项目当中去。
只有适合自己团队的规范才是好规范,你可能不喜欢BEM,你也可以去选择一些其他的命名规范(比如NEC)。团队最重要的是统一,大家统一用一种套路,一种规范,这样上下游合作,内部合作都会极大地降低沟通成本。
由于本人水平有限,如果文中有错误的地方欢迎大家指点勘误,谢谢!
参考资料:
BEM
理解 CSS 命名规范 --BEM
编写模块化CSS:BEM
决战BEM, 10个常见问题及其解决方案
如何看待 CSS 中 BEM 的命名方式