背景
是否有被css支配过的恐惧?
是否在开发较大型网站时,因为css架构不够强大而给自己挖坑的经历?
是否在维护别人代码时,因为害怕破坏别的东西而不敢下手?
又是否在修改css样式时,不经意间就引发可怕的蝴蝶效应?
如果你多少有过这些经历的话,那接下来文章兴许能解救你于这尴尬的境地中。
CSS架构
css的编写很容易,但在大规模网站开发的时候,经常发现花费大量时间在阅读、维护和重构上,而不是在编写和添加新功能。所以拥有一套强大的css架构体系就变得 尤为重要。当前较为普遍的几种框架BEM、OOCSS、 SMACSS。
OOCSS(Object Oriented CSS)即:面向对象的css,其主要原则是:
分离结构和主题
分离容器和内容
<div class="media media-shadow">
<div class="media-image-container">
<img class="media-image" src="rean.jpg" alt="">
</div>
<div class="media-body">
<p class="media-text">xxxxxxx</p>
</div>
</div>
形如上面的结构,media作为一个整体,media-shadow
作为结构的可拆分主题样式,整体样式结构不依赖于其他结构,与容器分离。
SMACSS(Scalable & Modular Architecture for CSS),即css的可拓展和模块化架构。
- 为css分类
(Base(基础样式)、Layout(布局样式)、Module(模块样式)、State(状态样式)、Theme(主题样式));
- 命名规则
按照以上五种分类命名,base使用标签
p
、a
等,layout使用l-
或者layout-
前缀,module使用media-
,状态使用is-
,主题theme-
; - 最小化适配深度
形如这样的样式结构,为减低对html结构的依赖,则选择下面第二种样式规则。
/* style 1 */
.sidebar ul h3 { }
/* style 2 */
.sub-title { }
BEM,则是本篇文章主要讲述的内容,将在以下部分详述~
BEM是什么
BEM是由Yandex团队(俄罗斯最著名的互联网企业,旗下Yandex浏览器在当地市场占用率超过Chrome)提出的前端命名方法论,意思就是块(block)、元素(element)、修饰符(modifier),BEM思想让css类更加透明更加有意义。
先看一段css代码:
.block {/* styles */ }
.block__element {/* styles */ }
.block--modifier {/* styles */ }
以上是BEM的约定命名模式,其中:
- .block表示模块、组件
- .block__element代表.block的后代,用于形成一个完整的.block组件
- .block–modifier代表.block的不同状态或版本
之所以使用两个连字符和下划线,是为了自定义块时可以使用单个连字符,eg:
.site-block {/* styles */ }
.site-block__input {/* styles */ }
.site-block--focus {/* styles */ }
为什么使用BEM
初次接触BEM时也许会觉得这种命名方式丑陋,让html变得臃肿,这也是反对BEM最常见的观点。
我们回到最初的问题,在css开发过程中最大的问题是阅读和维护。OOCSS和SMACSS诚然也是站在模块化的角度架构,但OOCSS追求元件的复用,class命名也比较抽象,不能体现具体内容,css与html的耦合度太紧,甚至于也许定义了许多css元件,但可能永远不会被用到,如bootstrap。SMACSS则是站在语义化css和html的角度,并且也减少了对html结构的依赖性,降低css和html的耦合度,但也由于命名规则并不需要严格遵守,按个人喜好定义,所以在团队开发工程中也会带来问题。
BEM约定的class命名方式则携带了更多的有用信息,也极大程度减少了样式冲突,使得代码更易于维护。在团队开发工程中,新接手的开发人员能很容易从class命名上分辨出哪部分是block,对应哪些element,有哪些modifier,从而提取出可以独立使用的HTML。
怎么使用BEM
块(Block)
一个块就是一个组件,下面将通过详细例子来描述。
形如上面样式结构,在开发中使用的频率也是比较高的,在BEM思想中,红框内的整体被视为一个块,内部的各个元素脱离了这个块基本意义不大,所以将这个整体定义为一个块:
.list {/* styles */ }
元素(Element)
元素是块的子节点,通过__element
的方式表明某一段html是一个元素,如上图,左右两侧的则可以分别以此命名;
<div class="list">
<img class="list__img" />
<div class="list__info"></div>
</div>
以上命名方式,不论是在HTML还是在CSS中都能立即分辨出list__img
list__info
是list
的子节点。BEM元素的两个特点即是:
- 让css的优先级保持相对扁平
- 可以知道哪些东西是一个子元素、
值得注意的是,随着HTML结构的复杂度增加,页面也不仅仅只是元素和子元素的关系,还有孙元素,甚至于曾孙元素,对于此类元素不要链式命名BEM元素,eg .form__row__input
,形如这样的冗长的链式命名是不合适的。所以通过:
- 只把子孙元素链接到有意义的块;
- 创建新的块来保存元素
这两个方法来避免链式命名。举个栗子,形如上图的页面结构,整体是一个列表,所以实际操作中,dom结构要比上面的更为复杂。eg:
<div class="block">
<ul class="block__list">
<li class="block__item">
<!-- dom -->
</li>
</ul>
</div>
上述代码中,.block__list
是.block
的子元素,而.block__item
是.block
的孙元素,或者说是.block__list
的子元素,要避免链式命名,这个结构中没有其他冲突元素,所以可以自己直接将他们当成.block
的子元素,即把.block__item
链接到.block
上,形成一个有意义的组件。
又形如像这样的一个dom结构,
<section class="comments">
<h2 class="comments__title"></h2>
<article class="comments__comment">
<h3 class="comments__comment__title"></h3>
</article>
<article class="comments__comment">
<h3 class="comments__comment__title"></h3>
</article>
<!-- ... -->
</section>
这里链接孙元素其实意义不大,如果创建一个新的块来保存孙元素则更有意义,eg:
<section class="comments">
<h2 class="comments__title"></h2>
<article class="comment">
<h3 class="comment__title"></h3>
</article>
<article class="comment">
<h3 class="comment__title"></h3>
</article>
<!-- ... -->
</section>
修饰符(Modifier)
修饰符是改变某个块的外观标志,使用--modifier
添加到块中。下面将举例一些典型的使用修饰符的情况。
button
在一个页面结构中,可能会使用到不同状态的按钮,怎么将bem修饰符添加到html中呢?eg:
<button class="button">Primary button</button>
<button class="button button--secondary">Secondary button</button>
.button { padding: 0.5em 0.75em; background-color: red; }
.button--secondary { background-color: green; }
.button--secondary
增加了一个红色的按钮状态,通过HTML可以很明显知道,.button--secondary
是button
的另一个状态。作为传统的BEM思想,这样可以很直观,也确保了css的简洁。但也许会觉得这样的HTML还是不够简洁,理想状态应该是:
<button class="button">Primary button</button>
<button class="button--secondary">Secondary button</button>
但这样就不能保证css的简洁,这种情况也是可以解决的,
1、如果使用sass或其他预处理器,可以使用mixin
来封装。eg:
@mixin button { padding: 0.5em 0.75em; }
.button { @include button; background-color: red; }
.button--secondary { @include button; background-color: green; }
2、普通CSS使用 CSS 属性选择器,eg:
class*='button']:not([class*='button__']) {
padding: 0.5em 0.75em;
}
以上两种方法并不是必须的,完全可以就使用传统BEM的命名方式。
BEM不足
了解完以上BEM的相关知识以后,你会发现,其实bem极大的提高了阅读性和降低维护成本,但是同时应该也会有一些疑问;
该如何自定义更具有意义的块呢?
如何管理块与块之前的关系呢?
该如何确保class类是否使用了JavaScript呢?
前面了解了SMACSS,是否有发现它所涉及的命名规则正好能比较恰当的解决上述问题?这就是命名空间。
命名空间
如果有留心的话,会发现不同网站会有不同的对命名空间的定义,也代表了不同的含义,以下列表是节选了我觉得比较合理并能大部分解决现有业务的列表。
命名空间列表:
- l-: 布局(layouts)
- o-: 对象(objects)
- c-: 组件(components)
- js: js的钩子(JavaScript hooks)
- u-: 实用类(utility classes)
l-: 布局(layouts)
表层与结构分离,即影响块或者元素的位置的属性需要被抽象为一个单独的类利于复用。分为全局布局和块级布局。
全局布局:作用于所有页面的布局,使用于大型网格容器,eg: 依旧形如上面的例子,包裹列表的容器可以视为一个全局布局,通常包含边距,背景,颜色等可用于大部分页面的布局;
<div class="l-wrap">
<!-- html -->
</div>
.l-wrap {
padding: 1.5rem;
background: #f0f0f0;
}
块级布局:每个块可能拥有自己的布局结构,作为一个最小的构建块,其实也就是对象和组件,即下文提到的o-
、c-
。
o-: 对象(objects)
对象布局作为最小的块,
- 可以被单独使用于任何地方,即上下文独立;
- 当然它们里面不能再包含其他的对象和组件;
- 使用o-最为前缀。
eg: buttons
是一个很典型的最小对象,不同的场景下不同的按钮状态,把它独立为一个对象构建块:
<a href="#" class="o-button">A button</a>
c-: 组件(components)
- 可以也被整体单独使用于任何地方,但它们是上下文感知的;
- 可以包含其他的对象和组件;
- 使用c-最为前缀
eg: 形如上面的元素结构,也可单独作为一个组件使用
<div class="c-list">
<img class="c-list__img" />
<div class="c-list__info"></div>
</div>
js: js的钩子(JavaScript hooks)
javascript钩子(.js)表示对象/组件是否需要JavaScript;
<div class="o-countdown jsCountdown">
<!-- ... -->
</div>
可以看到,使用JavaScript命名空间的好处是可以将js功能分开,易于维护。即使将来需要更改样式,也不至于破坏JS功能。
u-: 实用类(utility classes)
实用类是用来表现样式的一个非常好的辅助类,通常只包含一个属性,并且包含!important声明。eg:
.u-text-left { text-align: left !important; }
.u-text-center { text-align: center !important; }
总结
命名空间填补了BEM的遗憾,BEM加命名空间,解决了css难维护,难阅读,难复用的问题,提高了代码的通用性,也降低了css与html的耦合度。BEM传达的是一种思想,一种规范,让代码更具备可读性和可维护性;准确地说,其中BEM应该是一个完整的前端开发理论,不仅限于css。