BEM风格规范指的是 Block、Element、Modifier 这三者的简称,这个规范将 CSS 拆分成块、元素、修饰符,根本作用是帮助开发者快速理解HTML与 CSS 之间的关系。那么通过使用 BEM 能获得到什么好处呢?我罗列了下面几点:
在没有接触 BEM 之前,也许你会对我这罗列的这一堆优点一头雾水,这很正常让我们接着往下看,在看完本文的剩余内容时也许回过头你就能恍然大悟。
需要注意的是 BEM 并非是官方的风格规范,它是由Yandex团队开发,程序员之间约定俗成的一种契约规范,受众范围十分广泛,许多知名开源 UI 框架将其使用,如 Elementui、vant、uView-ui。但随着 tailwind 这类 CSS 框架的强势入局,BEM使用也有减少的趋势,毕竟只有在手动编写CSS代码的时候才用的上,如新起之秀,号称最完整Vue UI套件的 primevue。
要探究BEM怎么用,最简单的方法就是直接上手,让我们从编写一套有着丰富样式按钮组件开始,该组件包含最常见的颜色类型/大小切换功能。
可以看到属于 .btn 元素下的子项目,在命名时带上以父元素为头,__
分割的命名,而按钮的不同状态颜色则通过基本样式名为头, --
分割进行命名。以这样的命名方式,翻译过来就是基本样式名被称为块,__
分割被称为元素,--
分割则被称为修饰符。
<article class="btn-demo">
<div class="btn btn--primary btn--large">
<span class="btn__icon">$</span>
<span class="btn__text">按钮</span>
</div>
<div class="btn btn--info">
<span class="btn__icon">$</span>
<span class="btn__text">按钮</span>
</div>
<div class="btn btn--mini">
<span class="btn__icon">$</span>
<span class="btn__text">按钮</span>
</div>
<div class="btn btn--warn">
<span class="btn__icon">$</span>
<span class="btn__text">按钮</span>
</div>
<div class="btn btn--danger">
<span class="btn__icon">$</span>
<span class="btn__text">按钮</span>
</div>
</article>
@mixin mBtnType($m, $color, $hoverColor, $activeColor, $fontColor: #FFF) {
.btn--#{$m} {
color: $fontColor;
border-color: $color;
background-color: $color;
&:active, &:hover {
color: $fontColor;
}
&:hover {
border-color: $hoverColor;
background-color: $hoverColor;
}
&:active {
border-color: $activeColor;
background-color: $activeColor;
}
}
}
.btn-demo {
display: grid;
grid-template: 30px / repeat(4, 25%);
grid-auto-rows: 30px;
grid-row-gap: 20px;
justify-content: space-between;
justify-items: center;
position: fixed;
left: 50%;
top: 20%;
width: 450px;
transform: translateX(-50%);
}
.btn {
$activeColor: #409EFF;
box-sizing: border-box;
display: flex;
justify-content: space-between;
align-items: center;
width: 80px;
height: 32px;
padding: 5px 10px;
border: 1px solid #F0F0F0;
border-radius: 4px;
color: #333;
background-color: #FFF;
cursor: pointer;
&:active, &:hover {
color: $activeColor;
border-color: #A0CFFF;
background-color: #ECF5FF;
}
&:active {
border-color: $activeColor;
}
&__icon {
font-weight: bold;
}
}
.btn--mini {
width: 60px;
height: 28px;
font-size: .8em;
}
.btn--large {
width: 100px;
height: 40px;
font-size: 1.1em;
}
@include mBtnType("primary", #409EFF, #79BBFF, #337ECC);
@include mBtnType("warn", #E6A23C, #EEBE77, #B88230);
@include mBtnType("danger", #F56C6C, #F89898, #C45656);
示例中样式使用的是 Sass,如需查看 CSS 请安装 Sass 包将其转换成传统 CSS 代码。
# 安装Sass
npm i sass -g
# 将Sass编译成CSS
sass input.scss output.css
简单来说,BEM 最核心的写法就是用上 __
与 --
将样式命名进行区分。
这是个很冷笑话式的问题,在任何时间地点不使用BEM规范进行约束都不会让你少个胳膊少个腿,这些是约束自我的规则,其价值来自遵循它们,当然前提是你的项目规范支持你这么做,不然你的同事可能不介意帮你少个胳膊,哈哈😉
如果你不喜欢这么做,那大可不用纠结于此规范。
BEM模块化的规范必定会带来一些坏处,最明显的一个问题就是容易将名称命名过长,看起来臃肿而凌乱,这也是最常被用来反对 BEM 的论点,在我看来,这其实即使优点也是缺点,你也不希望这么一个选择器 p li .title {}
与你 HTML 结构命名一样的业务模块发生样式污染吧?
如果坚定了决心,那么在不使用BEM规范的情况下,还有什么比较好的CSS命名方法吗?以下面的示例演示,可以这么去做。
<article class="fruit-select">
<div class="cell-list">
<div class="cell-item">
<span class="cell-text">香蕉</span>
<input type="checkbox" class="cell-checkbox">
</div>
<div class="cell-item active">
<span class="cell-text">榴莲</span>
<input type="checkbox" class="cell-checkbox">
</div>
<div class="cell-item">
<span class="cell-text">柚子</span>
<input type="checkbox" class="cell-checkbox">
</div>
</div>
</article>
将HTML元素辅助的作用进行语义化,对CSS类名进行一个简单的命名,对于不同状态样式一块,考虑使用交集选择器进行编写。
.fruit-select {
width: 200px;
padding: 10px 0;
border: 1px solid #666;
}
.cell-list {
display: flex;
flex-direction: column;
}
.cell-item {
box-sizing: border-box;
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 40px;
padding: 0 10px;
}
.cell-item + .cell-item {
border-top: 1px solid #666;
}
.cell-item.active {
color: #FFF;
background-color: #FFA500FF;
}
当然这其实也是一个建议,至少在我没有接触到BEM规范之前的一段时间,我都是这么去书写我的代码。
BEM 毕竟只是应该风格规范,只要有需要手动编写样式的地方,就能发挥 BEM 的威力,将 Sass 与 BEM 搭配可大大提高开发效率,可谓是锦上添花。
在一开始的 二、BEM要怎么用? 示例代码中,就用到了 Sass,而 Sass 应用在前端框架中可以说是最常见的操作,接下来将完整的以 Vue + Sass 实现一个表格页面为例。
<script lang="ts" setup>
/*
* 组件名: bem
* 组件用途: bem规范示例页
* 创建日期: 2023/12/27
* 编写者: XianZhe
*/
import { reactive } from "vue";
const $state = reactive({
source: [
["阿联酋迪拉姆", 193.89, 192.29, 195.25, 198.79, 193.42, "2023-12-27"],
["澳大利亚元", 486.94, 486.44, 490.2, 491.62, 485.15, "2023-12-27"],
["加拿大元", 539.83, 539.28, 543.45, 545.02, 538.58, "2023-12-27"],
["瑞士法郎", 834.27, 833.6, 840.13, 842.47, 832.27, "2023-12-27"],
["丹麦克朗", 105.47, 105.28, 106.31, 106.82, 105.11, "2023-12-27"],
["欧元", 786.92, 780.98, 792.43, 794.48, 784.52, "2023-12-27"],
["英镑", 905.71, 904.78, 911.78, 914.41, 904.36, "2023-12-27"],
["港币", 91.34, 91.32, 91.68, 91.68, 90.9, "2023-12-27"],
["印尼卢比", 0.0461, 0.0447, 0.0466, 0.0482, 0.0461, "2023-12-27"],
["日元", 4.9913, 4.9912, 5.0247, 5.0267, 4.9951, "2023-12-27"],
["美元", 713.21, 713.05, 716.05, 716.05, 710.02, "2023-12-27"]
]
});
</script>
<template>
<section id="bem" class="bem">
<header class="bem__hd">
<h2>BEM规范表格页</h2>
</header>
<main class="bem__bd">
<table class="bem__table">
<thead class="bem__table__hd">
<tr>
<th>货币名称</th>
<th>现汇买入价</th>
<th>现钞买入价</th>
<th>现汇卖出价</th>
<th>现钞卖出价</th>
<th>中行折算价</th>
<th>发布日期</th>
<th>操作</th>
</tr>
</thead>
<tbody class="bem__table__bd">
<tr v-for="(item, index) in $state.source" :key="index">
<td v-for="sitem in item" :key="sitem">{{ sitem }}</td>
<td>
<div class="bem__table__operation">
<span class="bem__table__operation-btn bem__table__operation-btn--danger">删除</span>
<span class="bem__table__operation-btn bem__table__operation-btn--primary">详情</span>
</div>
</td>
</tr>
</tbody>
</table>
</main>
<main class="bem__ft">
<button class="bem__btn">你好</button>
</main>
</section>
</template>
<style lang="scss" scoped>
.bem {
&__bd {
margin-bottom: 20px;
}
&__table {
border-collapse: collapse;
border: 1px solid #000;
&__hd {
background-color: #F0F0F0;
}
&__bd {
tr {
border-top: 1px solid #000;
}
}
&__operation {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px;
&-btn {
cursor: pointer;
}
&-btn--primary {
color: #409EFF;
}
&-btn--danger {
color: #F56C6C;
}
}
td {
min-width: 100px;
text-align: center;
}
}
&__btn {
width: 100px;
height: 38px;
border: unset;
color: #FFF;
border-radius: 4px;
background-color: #409EFF;
cursor: pointer;
&:hover {
background-color: #79BBFF;
}
&:active {
background-color: #337ECC;
}
}
}
</style>
再来看看 ELement-ui 关于 BEM 应用的部分源码,以供更多参考,此源码取自 ELement-ui 级联组件部分。Element-ui 源码对于 BEM 的应用比较高级,简单来说就是利用 Sass 的 Mixin(混合),将 BEM 拆分成 b、e、m 三者的混合函数,再使用 include 注入到每一个组件之中。
@include b(cascader) {
@include set-component-css-var('cascader', $cascader);
display: inline-block;
vertical-align: middle;
position: relative;
font-size: getCssVar('font-size', 'base');
line-height: map.get($input-height, 'default');
outline: none;
&:not(.is-disabled):hover {
.#{$namespace}-input__wrapper {
cursor: pointer;
box-shadow: 0 0 0 1px getCssVar('input', 'hover-border-color') inset;
}
}
.#{$namespace}-input {
display: flex;
cursor: pointer;
.#{$namespace}-input__inner {
text-overflow: ellipsis;
cursor: pointer;
}
.#{$namespace}-input__suffix-inner {
.#{$namespace}-icon {
height: calc(100% - 2px);
svg {
vertical-align: middle;
}
}
}
...
}
...
}
与之类似的还有Less这个CSS预处理器,使用起来和Sass是一样的道理。
看到下面这个选择器.cell__item--active
,你会期待什么,正确的做法是将.cell__item--active
与 .cell__item
放到一块,不能因为样式一样则直接使用,公共样式应该改用其他选择器样式。
<article class="cell__list">
<div class="cell__item">
<p class="cell__item--active">List item 1</p>
<button>按钮</button>
</div>
<div class="cell__item">...</div>
</article>
还有存在这种情况,不同的块覆盖同一层级的修饰符,不要这么做。
<style>
.block__btn {
color: #333;
background-color: #FFF;
border: unset;
}
.block__inner .block__btn--primary {
background-color: #409EFF;
}
</style>
<div class="block">
<div class="block__inner">
<button class="block__btn block__btn--primary">按钮</button>
</div>
</div>
🍅因发布平台差异导致阅读体验不同,源文贴出:《CSS 小技巧之BEM规范》