提升CSS文件的可维护性--采用BEM进行CSS类进行命名

BEM(Block–Element-Modifier)是什么?

BEM是由Yandex公司推出的一套CSS命名规范,官方是这么描述它的:

BEM — Block Element Modifier is a methodology that helps you to create reusable components and code sharing in front-end development

-- BEM是一种命名方法,能够帮助你在前端开发中实现可复用的组件和代码共享。

BEM解决的问题

BEM是一种让你可以快速开发网站并对此进行多年维护的技术。
在实际的开发过程中,由于CSS的样式应用是全局性的,没有作用域可言。如果没有一套切实可行的CSS命名范式,那么很容易就出现了CSS类’命名冲突考’的可能性(尤其是团队开发时).
举个栗子: 团队项目中开发了一个弹窗组件,在现有页面中测试都没问题. 突然有一天产品有了新的需求,有需求那就的干呀, 开发二话不说直接开始开发.等写完代码后打开该页面一看,
这个弹窗组件,页面中样式都变样了. 开发难过了,一查问题,原来是弹窗组件和新需求的样式相互覆盖了,接下来就是修改覆盖样式的选择器…
又一段时间,又开发新需求,每次为元素命名都心惊胆战…开发页为解决这样的命名苦恼想了一些解决办法, 他在所以页面的冲突样式的选择器加上一些结构逻辑,
比如子选择器、标签选择器,借此让选择器独一无二。一段时间后,新同事接手跟进需求,对样式进行修改,由于选择器是一连串的结构逻辑,看不过来,嫌麻烦,
就干脆在样式文件最后用另一套选择器,加上了覆盖样式…接下来又有新的需求…最后的结果,一个元素对应多套样式,遍布整个样式文件…
在理想的状态下,我们都希望自己开发的一套CSS组件时,不必担心它是否与其他组件的样式发生冲突

BEM的解决知道

针对上面类似的问题,BEM解决这一问题的思路是,由于项目开发中,每个组件都是唯一无二的,其名字也是独一无二的,组件内部元素的名字都加上组件名,
并用元素的名字作为选择器,自然组件内的样式就不会与组件外的样式冲突了。这是通过组件名的唯一性来保证选择器的唯一性,从而保证样式不会污染到组件外。
这也可以看作是一种“硬性约束”,因为一般来说,我们的组件会放置在同一目录下,那么操作系统中,同一目录下文件名必须唯一,这一点也就确保了组件之间不会冲突。

如何使用BEM规范

BEM的命名规矩很容易记:block-name__element-name–modifier-name,也就是模块名 + 元素名 + 修饰器名

一般来说,根据组件目录名来作为组件名字:

比如分页组件:

/app/components/page-btn/

那么该组件模块就名为page-btn,组件内部的元素命名都必须加上模块名,比如:

<div class="page-btn">
   <button type="button" class="page-btn__prev">上一页</button>
   <!-- ... -->
   <button type="button" class="page-btn__next">下一页</button>
</div>

上面我们用双下划线来明确区分模块名和元素名,当然也可以用单下划线,比如page-btn_prev和page-btn_next。我们只需保留BEM的思想,其命名规范可以任意变通。
通过一段不正确的BEM命名范式来更好的理解BEM

<div class="page-btn">
    <!-- ... -->
   <ul class="page-btn__list">
       <li class="page-btn__list__item">
           <a href="#" class="page-btn__list__item__link">第一页</a>
       </li>
   </ul>
   <!-- ... -->
</div>

需要注意的是:
1. BEM的命名中只包含三个部分,元素名只占其中一部分,所以不能出现多个元素名的情况. 所以上述每一页的按钮名可以改成:page-btn__btn。
2. BEM是不考虑结构. 比如上面的分页按钮,即使它是在ul列表里面,它的命名也不应该考虑其父级元素。
当我们遵循了这个规定,无论父元素名发生改变,或是模块构造发生的改变,还是元素之间层级关系互相变动,这些都不会影响元素的名字。所以即使需求变动了,分页组件该有按钮还是要有按钮的,
DOM构造发生变动,至多也就不同元素的增删减,模块内名称也随之增删减,而不会出现修改名字的情况,也就不会因为名字变动,牵涉到JS文件的修改,或样式文件的修改。

BEM的诟病

BEM命名好长

由于BEM的命名中包含了模块名,长长的命名会让HTML标签会显得臃肿。但别忘记了,规范并不是一成不变的, 在实际使用中我们也可以做一些适合的修改的,比如Instagram团队使用的驼峰式:

.blockName-elementName--modifierName { /* ... */ }

还有单下划线:

.block-name_element-name--modifierName { /* ... */ }

还有修饰器名用单横线连接:

.blockName__elementName-modifierName { /* ... */ }

那么问题来了,BEM命名太长,我们是不是可以用子代选择器来代替BEM命名?下面讨论一下子代选择器带来的问题。
子选择器, 子代选择器的方式是,通过组件的根节点的名称来选取子代元素。按照这个思路,分页按钮样式可以这么写:

<div class="page-btn">
   <!-- ... -->
   <ul class="list"></ul>
   <!-- ... -->
</div>
.page-btn { /* ... */ }
.page-btn .list { /* ... */ }

HTML看起来美观多了,但这解决了样式冲突问题么?试想下,如果让你来接手这个项目,要增加一个需求,新增一个组件,你命名放心么?
你面临的问题是:你打开组件目录,里面有个分页组件,叫做page-btn,可是你完全不知道要怎么给新组件命名,因为即使新组件模块名与page-btn不一样,也不能保证新组件与分页组件不冲突。
比如新的需求是“新增一个列表组件”,如果该组件的名字叫做list,其根节点的名字叫list,那么这个组件下面写的样式,就很可能和.page-btn .list的样式冲突:

.list { /* ... */ }

这还仅仅只有两个组件而已,实际项目中,十几个或几十个组件,难道我们要每个组件都检查一下来“新组件名是否和以往组件的子元素命名冲突了”么?这不现实。
BEM禁止使用子代选择器,以上是原因之一。
第二个原因在于: 子代选择器不好的地方还在于,如果层次关系过长,逻辑不清晰,非常不利于维护(这一点在Google的优化建议中也提及到了)。
同时,如果我们在做一些动画时,如果一不小心要求还很高,如还hybird应用,那么过深的层级嵌套也会导致动画效果受到影响的.降低样式计算的范围和复杂度
为了懒得命名或者追求所谓的“精简代码”,写出下面这种选择器:

.page-btn button:first-child {}
.page-btn ul li a {}
/* ... */
/* 维护代码,新增需求 */
.page-btn .prev {}

用层次关系结构关系来定位元素,可能会因为需求改变而大面积的重写样式文件。试想一下维护这类代码有多么痛苦,我们要一边检查该元素的上下文DOM结构,一边对照着css文件,一一对比,
找到该元素对应的样式,也就是说我为了改一个元素的代码,需要不断翻阅HTML文件和CSS文件,可维护性非常之差。更有甚者,来维护这块代码的同事,直接在样式文件最后添加覆盖样式,
这会造成一个非常严重的问题了:同一个元素样式零散的分布在文件的不同地方,而且定位该元素的选择器也可能各不相同。
这样的样式文件只会越写越糟糕,可以说,当我们用子代选择器来定位元素时,这个样式文件就已经注定是要被翻来覆去的重构的了,甚至,每个来维护这个文件的人都会将其重构一遍。
子代选择器还会造成权重过大的问题,当我们要做响应式的时候,某个带样式的元素需要适配不同的屏幕,此时,我们还要不断的确认该元素之前的选择器写法!为了覆盖前面权重过大的样式,
甚至通过添加额外的类名或标签名来增加权重。可想而知,此后这个样式文件的维护难度就像雪球一样,越滚越大。如果我们用的是BEM,要覆盖样式很简单:找到要覆盖样式的元素,
得知它的类名,在媒体查询中,用它的类名作为选择器,写下覆盖样式,样式就覆盖成功了,不需要担心前面样式的权重过大。

BEM修饰器

根据不同的场景,组件可能会表现出不同的样式。比如分页组件在pc端具有具体的页码以及上下页按钮,但在移动端,因空间有限,可能只保留上下页按钮。
我们可以用修饰器来区分这两种情况。默认情况下,分页按钮的类名为page-btn,但在移动端,我们需要加多个类名page-btn–min

/* 缩小版分页组件中,具体页码按钮隐去 */
.page-btn--min .page-btn__btn {
    display: none;
}
.page-btn--min .page-btn__prev {
    width: 50%;
}
.page-btn--min .page-btn__prev {
    width: 50%;
}

上面这种情况用了子代选择器,BEM是不允许这么写的,BEM中修饰器的样式不依赖于任何结构关系,也就是说,元素的状态改变只会影响自身,不对其他元素进行影响,
但实际上,这很难做到的。以上的写法不会造成样式冲突的,而且权重的影响也不大。
BEM修饰器代表着元素的状态,但有时候元素的状态需要js来控制,此时遵循规范没有任何好处,比如激活状态,BEM推荐的写法是:

.block__element {
    display: none;
}
.block__element--active {
    display: block;
}

当用js为该元素添加状态时,我们需要知道该元素的名字block__element,这样我们才能推导出它的激活状态为block__element–active,这是不合理的,
因为很多时候我们无法得知元素的名称,所以这时候,我们应该统一js控制状态的类名格式,比如is-active、js-active等等,这些类名只用作标识,不予许有默认的公共样式:

.block__element {
    display: none;
}
.block__element.is-active {
    display: block;
}

原子类和BEM

BEM可以不需要用到原子类,但是如果已经引入了类似Bootstrap的框架,也没必要强制避免使用原子类,
比如“pull-right”、”ellipsis”、“clearfix”等等类,这些类非常实用,和BEM是可以互补的。
在组件开发中其实不推荐使用原子类,因为这会降低组件的可复用性。可复用性的最理想状态就是组件不仅仅在不同的页面中表现一致,在跨项目的情况下,也能够运行良好。
如果组件的样式因为依赖于某几个原子类就要依赖整个Bootstrap库,那么组件d 迁移负担就重很多了。
原子类更适合应用在实际页面中,这是因为页面变动大而且不可复用,假设在header中,我们用到了两个组件logo和user-panel(用户操作面板),两个组件分别置于header的左侧和右侧,我们可以这么写:

<div class="header clearfix">
    <div class="logo pull-left"><!-- ... --></div>
    <div class="user-panel pull-left"><!-- ... --></div>
</div>

header可以封装成一个模块,但它复用程度不高,不能算是组件,所以即使使用原子类也没有关系。在项目中,使用原子类之前应该考虑一下,这个场景是否变动大而且不可复用,如果是的话,我们可以放心的使用原子类。
组件应该是“自洽的”,其本身就应该构成了一个“生态圈”,也就是说,他几乎不需要外部供给,自给自足就能够运转下去。
在实际页面中也需要用到BEM命名方法,不然乱起的一个名字很可能就和某一组件冲突了,导致样式相互覆盖。
假如我们有联系页面,路径是/pages/contact/。那么该页面的模块名可以是page-contact,其名下元素均以page-contact__element-name命名。
一般来说,实际页面中只是对组件进行调用,对组件的位置进行调整,但不会对组件内部细节进行修改。但实际情况下,同一个组件在不同页面不同模样的情况也是有的,所以会出现在实际页面中对组件样式进行微调的代码:

/* 联系页面对分页按钮进行微调 */
.page-contact .page-btn {}

但更推荐的做法是给分页组件添加一个修饰器,将上面的样式放到修饰器名下,再根据实际情况运用到页面中。

最后总结一下: BEM保证样式不冲突的核心就是:在元素名中加入唯一的标识。这个标识在BEM中对应的是模块名,也可能是一个独一无二的乱序字符串

扩展阅读的相关链接:
BEM
BEM官方推广页
CSS Modules Welcome to the Future – CSS 模块
BEMIT: Taking the BEM Naming Convention a Step Further

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值