阅读 430

BEM命名规范

BEM命名规范是什么?

BEM是Block(块)、Element(元素)、Modifier(修饰符)的简写,是一种组件化的 CSS 命名方法和规范,由俄罗斯 Yandex 团队所提出。使用BEM主要是为了将用户界面划分成独立的块,使开发更为简单和快速,有利于团队协作,方便维护。

使用场景?

场景一:开发一个弹窗组件,在现有页面中测试都没问题,一段时间后,新需求新页面,该页面一打开这个弹窗组件,页面中样式都变样了,一查问题,原来是弹窗组件和该页面的样式相互覆盖了,接下来就是修改覆盖样式的选择器...又一段时间,又开发新页面,每次为元素命名都心惊胆战,求神拜佛,没写一条样式,F5都按多几次,每个组件都测试一遍...

场景二:承接上文,由于页面和弹窗样式冲突了,所以把页面的冲突样式的选择器加上一些结构逻辑,比如子选择器、标签选择器,借此让选择器独一无二。一段时间后,新同事接手跟进需求,对样式进行修改,由于选择器是一连串的结构逻辑,看不过来,嫌麻烦,就干脆在样式文件最后用另一套选择器,加上了覆盖样式...接下来又有新的需求...最后的结果,一个元素对应多套样式,遍布整个样式文件...

为何选择?

以往开发组件,我们都用“重名概率小”或者干脆起个“当时认为是独一无二的名字”来保证样式不冲突,这是不可靠的。

理想的状态下,我们开发一套组件的过程中,我们应该可以随意的为其中元素进行命名,而不必担心它是否与组件以外的样式发生冲突。

BEM解决这一问题的思路在于,由于项目开发中,每个组件都是唯一无二的,其名字也是独一无二的,组件内部元素的名字都加上组件名,并用元素的名字作为选择器,自然组件内的样式就不会与组件外的样式冲突了。

这是通过组件名的唯一性来保证选择器的唯一性,从而保证样式不会污染到组件外。

这也可以看作是一种“硬性约束”,因为一般来说,我们的组件会放置在同一目录下,那么操作系统中,同一目录下文件名必须唯一,这一点也就确保了组件之间不会冲突。

BEM规则

  • 块名称为其元素和修饰符定义了命名空间。
  • 块名称与元素名称之间用双连字符--分隔。
  • 块名称与修饰符或元素与修饰符之间用双下划线__分隔。
  • 命名一般使用小写字母。
  • 单词之间可以使用-分隔。

命名约定的模式有如下几种:

.block__element{}
.block--modifier{}
.block__element{}--modifier{}
复制代码

BEM命名好长

BEM的命名中包含了模块名,长长的命名会让HTML标签会显得臃肿。

其实每个使用BEM的开发团队多多少少会改变其命名规范,比如Instagram团队使用的驼峰式:

.blockName-elementName--modifierName { /* ... */ } 
复制代码

还有单下划线:

.block-name_element-name--modifierName { /* ... */ } 
复制代码

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

.blockName__elementName-modifierName { /* ... */ } 
复制代码

其实这些对缩短命名没有多大的帮助,但我们也无需担心文件体积的问题,由于服务端有gzip压缩,BEM命名相同的部分多,压缩下来的体积不会太大。另外现在都用IDE来编写代码了,有自动提示功能,也无须担心重复的输入过长的名字。

因为命名长,我们是不是可以用子代选择器来代替BEM命名?这样至少在HTML编写时,让HTML标签看起来美观一点。

下面说说子代选择器带来的问题。

子选择器

子代选择器的方式是,通过组件的根节点的名称来选取子代元素。按照这个思路,分页按钮样式可以这么写:

<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禁止使用子代选择器,以上是原因之一。子代选择器不好的地方还在于,如果层次关系过长,逻辑不清晰,非常不利于维护。为了懒得命名或者追求所谓的“精简代码”,写出下面这种选择器:

.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命名方法,不然乱起的一个名字很可能就和某一组件冲突了,导致样式相互覆盖。

假如我们有联系页面,路径是/pages/contact/。那么该页面的模块名可以是page-contact,其名下元素均以page-contact__element-name命名。

一般来说,实际页面中只是对组件进行调用,对组件的位置进行调整,但不会对组件内部细节进行修改。但实际情况下,同一个组件在不同页面不同模样的情况也是有的,所以会出现在实际页面中对组件样式进行微调的代码:

/* 联系页面对分页按钮进行微调 */ .page-contact .page-btn {} 
复制代码
文章分类
前端
文章标签