使用BEM声明CSS样式名

折腾CSS类名已经很长一段时间了,也得到了一些教训和经验。维护CSS有几个比较困难的问题:

  • 默认全局命名空间,样式冲突十分常见
  • 混乱的样式重用或选择器的滥用,导致改动牵一发而动全身
  • 修改样式表的同时可能会改动页面结构,而且往往不只一个页面

之前的关注点是通过选择器嵌套和样式复用来决定样式名,由于过分考虑样式的复用,又没有进行正确的限制,样式耦合十分严重。最后回过头一想,如果是使用类名来决定选择器的嵌套和样式重用,情形应该会怎么样呢?恰好之前也了解了一点BEM的东西,这篇文章是我关于BEM的思考和尝试。

<!--more-->

参考文档:

1. 理解BEM

在我的项目中,我使用_声明元素,使用-声明修饰符,连词之间使用驼峰,这个类名可能会有些“三不像”,但是只要明确了他们的含义并一直保持,跟这种命名方式带来的便利比起来,看起来丑陋的名字也就无关紧要了。

1.1. 块和元素

选择器并不需要完整反映页面结构的嵌套,而应该尽量精简。BEM尽管看起来是根据元素的嵌套来进行命名的,但实际上只有两层结构:块名和块下面的元素:

  • 一个块是一个独立的区域,就像页面的一块“积木”,一个块既可以是简单的也可以包含其他的块。
  • 元素是块的一部分,具有某种功能或位置含义。元素是依赖上下文的:它们只有处于他们应该属于的组件的上下文中时才是有意义的。

也就是说,没必要将块下面的每个标签都定义为元素(甚至没有必要为所有的标签都使用BEM命名),有的标签尽管出现在块的内部,但可以看作一个独立的组件或颗粒类,不需要使用_进行命名。最重要的是,不能出现B_E_E这样的类名,遇见这种情况,将元素转换成组件是更好的选择。

1.1.1. 块的独立性

为了详细的阐述我的理解,不厚道地把官网上的图拿过来了。 bemDemo 单单只考虑这张图的话,以独立的单词表示,以_连接块和组成这个块的元素,可以采用下面的类名和页面结构:

<div class="head">
    <ul class="menu">
        <li class="menu_item"></li>
        <li class="menu_item"></li>
        <li class="menu_item"></li>
        <li class="menu_item"></li>
    </ul>
    <div class="head_left">
        <div class="logo"></div>
    </div>
    <div class="head_main">
        <form action="" class="search">
            <input type="text" class="search_input">
            <button class="search_submit" type="submit"></button>
        </form>
    </div>
    <div class="head_right">
        <form action="" class="auth">
            <div class="auth_group">
                <input type="text" class="auth_input">
            </div>
            <div class="auth_group">
                <input type="text" class="auth_input">
            </div>
            <div class="auth_group">
                <input type="text" class="auth_button">
            </div>
        </form>
    </div>
</div>

样式表就应该为

.head{
    &_left {}
    &_main {}
    &_right {}
}
.menu {
    &_item {}
}
.search {
    &_input {}
    &_submit {}
}
.auth {
    &_group{}
    &_input {}
    &_submit {}
}

使用BEM,需要理解的是,每个块之间相互是独立的。 块中可以包含其他的块,每个块之间相互是独立的,块只决定了他包含的元素的样式(实际上样式表中,元素是不必嵌套在块中,这么做主要是为了可以使用&快速构建样式表)。

块的独立性的另一个含义是:在样式表中,声明块的前面不能有其他块的命名空间的限制,也就是说,块放在页面上的任何地方都可以维持他的基本样式。。

1.1.2. 不要使用相同的元素名

使用BEm时,尽管WebstromSCSS已经帮我们解决了大部分问题,单书写很长一段的限制类名前缀仍旧是一件让人苦恼的事情。也许你会想到,可以参考NEC那样使用u-hd,u-bd这样的类名作为元素名,然后通过块名的命名空间,使用后代选择器限制样式,这样就不需要写那么多元素名了嘛!(没错,这就是我之前趟过的坑。千万不要这么做!)。

原始的BEM禁止使用后代选择器,更准确的说法并不是禁止使用后代选择器,而是禁止在不同的块中使用相同的元素名。 原因是后代选择器存在潜在性的样式污染的风险,因为块中允许嵌套块,因此父块就可能通过后代选择器影响子块中的元素,这违背了块的独立性,带来的就会是无穷的灾难。

也许使用子代选择器>是一个解决办法。呃好吧,这个选择器受HTML结构的严重限制,总之,最好还是放弃使用相同的元素名吧,除了整个样式表看起来没有那么统一之外,暂时也没有发现什么坏处。

1.2. 修饰符

造成区分块和元素较困难的一种情形是:在head块中的auth块,也应用在其他地方比如foot,只不过有些许区别,这时候,相当于auth根据所处的块样式发生了改变,这时候应该将auth还看做是独立的块,还是依赖于head块的元素head_authfoot块的元素foot_auth呢?

答案是:将auth看做是一个独立的块!这也正是上面代码的做法,并且,这也正是修饰符存在的理由。

如果需要创建一个和已存在的块非常相似的块,只是外观或行为有些许改变,则可以使用修饰符;同理,也可以为元素添加修饰符。

最简单的修饰符大概莫过于数字了,这时候你想到了什么?没错,网格系统col-xs-3这样的类名,就可以看做是col这个类拥有两个修饰符xs3,至于他们的具体含义是什么,就需要我们自己定义了。

1.2.1. 避免结构语义化的修饰符

回到上面那个问题,为了区分headfoot块中的两个auth,我们可以添加后缀auth-inHeadauth-inFoot这两个修饰符,当然,这两个修饰符完全没有扩展性,如果在侧边栏也使用了auth-inHead块的样式,那么这个命名就容易让人混淆。

也就是说:修饰符尽量避免关联结构上的语义,比如-inHead这样的,哪怕是直接使用没有语义的数字来命名修饰符;而应当根据两个相似的块的实际样式差别来取舍,而不是这两个块所处的结构上的不同。

如果一个块在页面上大量应用,而另一个相似的块使用次数较少,可以考虑不设置这个使用次数较多的块的修饰符,而将他作为SCSS的继承基类,其他带修饰符的类通过继承该基类,然后在定制专属的样式。

1.2.2. 慎重使用实际语义化的修饰符

如果某个块在页面上大量使用,则这个块的名字不应该是“实际语义化”的。回到上面的例子,searchauth都包含元素_input,_submit,他们的本质都是form标签,因此,更明智的做法是都将他们看做是form块,而不是两个独立的块:

  • search对应form-inline
  • auth对应form-horizontal

这样,就可以统一他们的元素为form_groupform_input。至于_submit,不觉得把它们看做是独立的btn块更合适吗?当然,如果这个按钮的样式是这个块独有的,作为元素也是十分合适的,这样就回到了前面的问题:明智地决定一个标签到底是块还是元素!

1.3. 区分块和元素

正确区分块和元素是非常重要的。前面提到:

  • 元素是依赖于块的,只有处于他们的块中元素才是有意义的。
  • 块是由元素组成的,不存在没有任何元素的块(那个时候应该被称为颗粒类)

由于块可以嵌套块(这句话貌似出现了很多次),块和元素之间的界限比较模糊,怎么处理这个问题十分困难。

1.3.1. 选择性使用后代选择器

使用修饰符最大的困难就是找到一个合适的名称用来指定修饰符,同样是上面这个head块和foot块的auth问题,如果这两个略有差别的auth块的样式是他们父块所独有的,不在页面上其他地方被复用,可以考虑使用后代选择器而不是重新指定两个不同的修饰符来描述。

.head {
    .auth{}
}
.foot {
    .auth{}
}

使用后代选择器,就仿佛将一个块完全变成了父块的元素来指定样式,这似乎违背了“独立块”的原则(块前面不能有任何选择器的限制)和原始NEM中“禁止使用后代选择器”的原则。这么做有两个理由:

  • 考虑到这个块的独立样式不会被重用,单独声明一个修饰符,然后只使用一次是一件很浪费的事情。
  • 页面上一般不太会出现块的循环嵌套的问题,即很少出现一个块下面的某个子块是它本身的情况。如果我们严格使用B_E的方式声明块和元素,则不会出现使用后代选择器造成的样式污染问题。

因此,在声明块的基本样式的时候,应当保持块的独立性;而当父块严重影响(限定)这个块的某些样式的时候(导致他不会在其他地方被重用),使用后代选择器。

一个简单的判断方法是:如果想出一个合适的修饰符非常困难,正如BEM常见的10个疑问中第五条提到的“为了声明新的标识符,已经将这个块全部的样式都用掉了”(原文的建议的重新声明一个块),那么就使用后代选择器;如果明确知道样式被其他地方重用的可能性很大(浏览设计图),则使用修饰符。

1.3.2. 元素与块的多重性

是否允许一个标签同时拥有多个状态(多个块,或者即是块又是元素)?这个问题不是那么明显,举两个个例子:

  • 有时候发现某个块具有前面两个块的部分样式,比如listnav,可以简单将他们组合在一起然后进行一次样式覆盖,而不需要单独声明新的块(为什么写到这里我想到了I have a pen, I have an apple...)。
  • 由于强迫症的存在,我希望尽量精简HTML文件的结构(块的元素和子块存在同一个标签上),比如前面的head_left元素和logo块。

关于问题1,我的看法是:不允许一个标签同时存在多个块!即使一个块下面只包含一个块,完全可以使用修饰符进行处理。如果一个容器具有多个块,则内部的元素就会混乱,某些标签就可能成为不同块的元素,代码的逻辑立马变得复杂起来。

关于问题2,我的看法是:允许一个标签作为父块的元素和子块。前面提到,可以选择性使用后代选择器,这样的话,将元素与子块放在同一个标签上也是可以接受的,这样可以简洁页面的结构。但是,这并不意味着非得把只包含一个子块的元素修改到与这个子块同一个标签上。总之到底是父元素包含子块,还是父元素与子块公用标签,我并没有确切的结论,上面那篇文章提到“没有人拍着你的肩膀让你把这个div标签去掉”,大概的意思是完全保证块的独立性,包括HTML结构的独立性吧。

1.4. 颗粒类

颗粒类指的是那些完成独立功能,且不依赖任何父块或者限制任何子元素的独立样式类,比如整个网格系统和flexbox布局系统中的类,以及某些功能类,比如flfrclearfix等。 使用颗粒类可以比较灵活地使块具备通用样式,而无须单独再使用修饰符。某些地方颗粒类被称为原子类。 颗粒类也可以使用修饰符进行修饰,比如前面提到的.col-xs-3。一个块上,可以带有某些单独的样式,比如head块同时具备网格系统的container颗粒类。

1.4.1. 减小对颗粒类的依赖

颗粒类在某种程度上与行内样式无异,只是从重复书写样式转变到了重复引入类。表面上我们可以通过颗粒类方便地定制样式和组合样式,而无须在组件中重复书写样式,貌似大大提高了样式的重用。但是,一个组件应该是独立的,如果在组件上大量使用颗粒类,则去除这些颗粒类之后,组件可能就无法维持他最基本的样式了。

因此组件中诸如布局(浮动,定位,盒子模型)和尺寸(宽高,内外边距)等决定组件在文档流中的样式,最好不使用颗粒类;而诸如边框,颜色等基础样式,可以使用修饰符来代替颗粒类。

为了提高样式的重用,正确的做法应当使用在SCSS中使用混合器,而不是在HTML中使用颗粒类。

1.4.2. 标识类

修饰符用来表示块或元素的某些状态,其中,有一类状态,比如高亮,禁止这些状态,是多数元素都会存在的,因此为了方便(不需要在前面添加块_元素前缀,且方便JS动态设置),统一命名为is-activeis-disabled等,且这些类只作为标识,只依赖具体的块或元素名称,而不能有默认的公有样式。

为了避免违反前面"不要使用相同的元素名"的规则,标识符类应当作为多类选择器而不是后代选择器来使用。由于块是可以嵌套的,代表着一个块中可能同时存在数个active标识类,如果使用后代选择器且最外层的active生效了,则内部的active都可能受到污染。

1.5. 结论

上面啰里八嗦扯了一堆,总结了使用BEM比较困难的几个地方:

  • 区分块和元素
  • 区分修饰符和后代选择器
  • 修饰符的命名
  • 颗粒类的使用

如上面的内容提到的,有的问题已经有结论了,有的问题仍旧需要进一步的探索和思考。总之,尽管很早之前就开始纠结这个问题,然而正如前辈所言,样式表才是所有语言中最难维护的代码(如果CSS也算编程语言的话,好吧肯定有人说不算),长路漫漫,上下而求索也。

2. 名称库

NEC提供了许多的类名参考,根据需要可以拿来作为块名,元素名,和类名

  • 实际语义名用作块名()
  • 语义名和hd,sd等方位名可用作元素名(注意上面提到的坑)
  • 修饰符在实际需求中进行指定(可以跟据尺寸,方位和基础样式等命名)
2018年五月面试发现的一些问题 BFC及其应用