决战BEM, 10个常见问题及其解决方案

2017-04-14

无论您是刚刚发现BEM,还是老手,你可能欣喜的发现这是一个多么有用的方法论。 如果你不知道BEM是什么,我建议你去看看BEM官网,然后继续这篇文章,因为文章中的一些术语需要对这种CSS方法有基本了解。

本文旨在帮助BEM爱好者或者有兴趣了解它的开发者更有效的使用它。

我一直不认为他是一种最优雅方式来命名css样式。让我抛弃它这么长时间原因很多,其中有一点就是令人尴尬的语法。作为设计师的我不希望我性感的标题夹杂着肮脏的双重下划线和恶劣的双连字符。

但是作为开发者的我务实的看了一下,最终,构建用户界面的模块化方式和逻辑战胜了我右脑的抱怨。我确实不建议让它作为自己的客厅核心,但是当你需要一件救生衣(想象你在一个css的大海上),我想功能大于形式。无论如何,有的聊了。这里是我为之斗争的10个困境,及其解决方案

当遇到孙子(或者更下级)选择器时怎么办?

申明一下,当你需要引用嵌套两层深度的元素时就会用到孙子选择器。这些熊孩子是我生命中的祸害,我确信他们的滥用时人们厌恶BEM的原因之一。我给个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div class="c-card">
<div class="c-card__header">
<!-- Here comes the grandchild… -->
<h2 class="c-card__header__title">Title text here</h2>
</div>
<div class="c-card__body">

<img class="c-card__body__img" src="some-img.png" alt="description">
<p class="c-card__body__text">
Lorem ipsum dolor sit amet, consectetur
</p>
<p class="c-card__body__text">Adipiscing elit.
<a href="/somelink.html" class="c-card__body__text__link">Pellentesque amet
</a>
</p>

</div>
</div>

可以想像没这样的命名会很快失控。而且越嵌套名字越可恶,不可读。我试着用了一个很短的block名和bodylink等短元素名,但当初始block名是c-drop-down-menu 的时候会是什么样子?

我相信双下划线模式在选择器名称中只能出现一次。BEM 指的是 BlockElement–Modifier, 而不是 BlockElement__Element–Modifier。所以需要避免多级元素命名。如果你写出了一个很深的岁子级别命名那么你需要重新考虑一下你的组件结构了。

BEM并不严格的与DOM挂钩,所以元素层级嵌套的多少层也没关系。命名约定是为了更好的识别顶级组件模块如c-card.

下面是我怎么处理这个card组件的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div class="c-card">
<div class="c-card__header">
<h2 class="c-card__title">Title text here</h2>
</div>

<div class="c-card__body">

<img class="c-card__img" src="some-img.png" alt="description">
<p class="c-card__text">Lorem ipsum dolor sit amet, consectetur</p>
<p class="c-card__text">Adipiscing elit.
<a href="/somelink.html" class="c-card__link">Pellentesque amet</a>
</p>

</div>
</div>

这意味着所有的后代元素只会收到card模块的影响,所有我们可以将img移动到c-card_header里面去或者添加c-card_footer不会影响语义结构。

2,应该添加命名空间吗?

你可能已经注意到了我在代码示例中使用了c- 字母。这个c代表’component’,也代表着我的BEM命名空间的基调。这个想法源自Harry Robert的命名空间技术 ,它提高了代码的可读性。

类型 前缀 例子 描述
Component c- c-card 应用程序的主干,包含所有组件的修饰前缀
Layout module l- l-grid 这些模块没有修饰前缀,纯粹用于定位c-组件和布局
Helpers h- h-show 具有单个功能,通常会用!important来提高特异性
States is- is-visible 组件的状态
JS hooks js- js-tab 拥有js行为的组件

我发现用了这样的命名空间是我的代码可读性大大提高。即使我不会在BEM上打包卖给你,但它的确是一个关键的外卖。你可以采用其他的命名空间,像qa-来表示 qa测试,ss- 来和服务器挂钩。但是上面列表里面是一个恒好的起点,你可以在使用BEM时候将他介绍给你的同事。

在接下来的一个问题里你就会知道这命名空间是一个很好的解决方案

我该怎么命名我的容器

很多组件都需要一个父容器来标识字元素的布局。这种情况下,我们总是试图将这个容器抽象为一个布局模块,比如l-grid 然后将其他模块作为l-grid_item的内容插入其中。

在我们的例子中,如果我们想要一个卡片列表。我们可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<ul class="l-grid">
<li class="l-grid__item">
<div class="c-card">
<div class="c-card__header">
[…]
</div>
<div class="c-card__body">
[…]
</div>
</div>
</li>
<li class="l-grid__item">
<div class="c-card">
<div class="c-card__header">
[…]
</div>
<div class="c-card__body">
[…]
</div>
</div>
</li>
<li class="l-grid__item">
<div class="c-card">
<div class="c-card__header">
[…]
</div>
<div class="c-card__body">
[…]
</div>
</div>
</li>
<li class="l-grid__item">
<div class="c-card">
<div class="c-card__header">
[…]
</div>
<div class="c-card__body">
[…]
</div>
</div>
</li>
</ul>

现在你应该形成一个固有思想,布局模块和组件命名空间怎么结合使用了。

不要害怕使用一些额外的标记来解决麻烦。没有人会拍着你的后背让你删去那几个div标签的。

在某些情况,这是不现实的。例如:你的栅格系统没有给你带来你想要的结果,或者只是淡淡想要一个语义去命名一个元素,你应该怎么做呢?我倾向于根据场景来选择list 还是container.同样一我们的卡片例子来说,我可能会使用<div class="l-card-container"></div> 或者<ul class="l-card-list"></ul> 关键还是在于约定保持一致性。

“跨组件”

另外一个问题就是,一个组件的样式和布局可能会收到其父组件的影响。这个问题的解决方案Simurai给出了详细的解答。这里我说说我认为最具扩展性的方法。

我们假设我们要在前面的例子里c-card__body添加一个c-button.该按钮已经自成一个组件

1
2

<button class="c-button c-button--primary"> Click me!</button>

如果没有和常规按钮有样式的差异则没问题,我们只需要这样放就可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div class="c-card">
<div class="c-card__header">
<h2 class="c-card__title">Title text here</h3>
</div>

<div class="c-card__body">

<img class="c-card__img" src="some-img.png">
<p class="c-card__text">Lorem ipsum dolor sit amet, consectetur</p>
<p class="c-card__text">Adipiscing elit. Pellentesque.</p>

<!-- Our nested button component -->
<button class="c-button c-button--primary">Click me!</button>

</div>
</div>

然而,问题就出在现实中会有一些微妙的样式差异。如,我们希望在c-card组件里 按钮小一点,具有圆角。

在此之前,这种跨组件类最强大的解决方案是:

1
2
3
4
5
6
7
<div class="c-card">

<div class="c-card__body">
<!-- My *old* cross-component approach -->
<button class="c-button c-card__c-button">Click me!</button>
</div>
</div>

这就是BEM官网所说的‘混合’。但是根据Esteban Lussich的一些评论,我已经改变了我对这种做法的立场。

在上面的实例中,c-card__c-button类会修改现有c-button一个或者多个属性,但是它会受限于权重优先级(甚至是特定性)。c-card__c-button类直邮在源码中的c-button声明之后才可以生效。当你构建更多的类似的跨组件的时候就会迅速失控。(有人会说!important可以解决,但是我不一定会鼓励这么做)

真正模块化的UI元素的修饰不应该去关心元素的父容器。无论你放在哪,它们应该看起来是一样的。使用‘混合’的方式从另一个组件添加一个类违反了组件驱动设计的开闭原则–即不应该依赖另一个模块来实现。

最好的办法就是使用一些修饰符来弥补这些小差异。随着项目的增长,你会发现你希望在其他地方重新使用它们。

1
<button class="c-button c-button--rounded c-button--small">Click me!</button>

即使你不会再使用它们,至少它也不会和父元素捆绑到一起,依赖特异性和权重优先级。

当然另一个选择时,回到你的设计师思维告诉他们这些按钮应该与网站上的按钮一致,并且完全避免这个问题。

5、”添加修饰符还是新建一个组件”

这是决定一个组件的结束,新组件的开始的最重要问题之一。在c-card例子里面,你可能还会创建一个c-panel具有非常相似的样式属性,但也有一些明显的差异。

是什么决定了,是新开一个c-panel呢还是,针对独特样式添加修饰符呢?

我们很容易过分的模块化并让所有内容成为组件。我建议我们尽量先考虑使用修饰符,但是如果你发现你的特定组件css文件变得那一管理的时候,这可能就是放弃修饰符的时机了。一个很好的指标是,当你发现自己为了设计新的修饰符,必须重新设置所有’block’级别 css的时候,就是开新组件的时机。

最后,如果你和其他开发人员或者设计师合作的时候,你可以让他们发表意见,抓住他们几分钟并且讨论出一个方案。这个方案可能是一个很好的方案,但是对于大型的应用来说,最重要是还是开发人员要理解什么模块是可用的,什么时候需要抽象出一个组件。

6、“怎么处理状态”

这是一个常见的问题,特别是当你在为一个组件处于active或者open添加样式的时候。加入我们的卡片又一个‘active’状态。我们点击的时候它展示出一个好看的边框造型。我们怎么去命名类名呢?

我的答案是,你有两个选择:

1
2
3
4
5
6
7
8
9
<!-- standalone state hook -->
<div class="c-card is-active">
[…]
</div>

<!-- or BEM modifier -->
<div class="c-card c-card--is-active">
[…]
</div>

虽然我喜欢保持BEM的命名一致性,但独立类的好处在于它可以方便的使用js将通用的状态钩子应用于任何组件。但当你脚本里必须使用特定修饰符状态类名的时候会有问题,这意味你需要编写更多的js代码。坚持一套标准状态钩子是有道理的克里斯·皮克斯(写了一个列表)[https://github.com/chris-pearce/css-guidelines#state-hooks],我建议你看看。

7、什么时候元素可以不添加类?

可以理解人们在构建复杂UI的时候已经被庞大而复杂的css类名数量所吓倒,特别是如果他们不习惯给每个标签都加上类名。

通常,我会在组件的上下文中将需要样式化的标签都加上类名。我经常很少给p标签加样式名。除非该组件要求他们在上下文中独一无二。

这可能意味着你的标记将包含很多类名。尽管如此你的组件将独立存在,并且在任何时候被废弃也不会有副作用的风险。

由于css的全局属性,给所有元素添加类名将使我们能控制组件如何呈现。最初造成精神上的不适对全模块化系统有好处。

8、如何嵌套组件

假设我们要在我们的c-card组件中显示一个清单。给一个demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<div class="c-card">
<div class="c-card__header">
<h2 class="c-card__title">Title text here</h3>
</div>

<div class="c-card__body">

<p>I would like to buy:</p>

<!-- Uh oh! A nested component -->
<ul class="c-card__checklist">
<li class="c-card__checklist__item">
<input id="option_1" type="checkbox" name="checkbox" class="c-card__checklist__input">
<label for="option_1" class="c-card__checklist__label">Apples</label>
</li>
<li class="c-card__checklist__item">
<input id="option_2" type="checkbox" name="checkbox" class="c-card__checklist__input">
<label for="option_2" class="c-card__checklist__label">Pears</label>
</li>
</ul>

</div>
<!-- .c-card__body -->
</div>
<!-- .c-card -->

这里有几个问题。一个我们在第一节介绍过的孙子选择器问题,第二是所有运用于c-card__checklist__item的所有样式仅适用于这个具体的用例,不能重复使用。

我偏向于将列表本身分解成布局模块,将清单的单项分解称自己的组件,使其能够在其他的地方独立使用。这也是我们的命名空间重新起作用:

1
2
3
4
5
6
7
8
9

<li class="l-list__item">

<div class="c-checkbox">
<input id="option_2" type="checkbox" name="checkbox" class="c-checkbox__input">
<label for="option_2" class="c-checkbox__label">Pears</label>
</div>

</li>

这样就可以避免样式重复,也就意味着可以在其他地方使用l-listc-checkbox.这的确意味着需要更多的标记,当时者相对于可读性,封装和可重用性来说代价是很小的。

9、组件不应该有很多类?

有人认为,每个元素有很多类,不是很好。而且--modifier可以叠加起来。就个人而言,我没有发现这有什么问题,因为这意味着代码更具可读性,而且我确切的知道了它要干什么。
来个示例,需要四个类来设置按钮的情况:

1
<button class="c-button c-button--primary c-button--huge  is-active">Click me!</button>

我得到这种语法不是最好看的,但它是明确的。

如果这些让你很头痛,你可以看一下Stergey Zarouski 的扩展技艺 。本质上,我们可以使用.className[class^="className"],[class*="className"] 来模拟vanilla CSS中的扩展功能。你看着眼熟那是因为它非常类似于Icomoon 处理图标的选择器方式。

这里有必要解释一下这个扩展技术,还记得sass或者less的时候经常会用到@extend吗?

1
2
3
4
5
6
7
8
9
.c-button {
border: 1px solid grey;
font-size:14px;
}
.c-button--primary{
@extend .c-button;
border: 4px solid orange;
backgroud-color: orange;
}

预处理之后是这样:

1
2
3
4
5
6
7
8
9
.c-button, .c-button--primary {
border: 1px solid grey;
font-size:14px;
}
.c-button--primary{
@extend .c-button;
border: 4px solid orange;
backgroud-color: orange;
}

这里我们可以用原生的css选择器来模拟上述的行为:

1
2
3
4
5
6
7
8
.c-button, [class^="c-button--"], [class*=" c-button--"] {
border: 1px solid grey;
font-size:14px;
}
.c-button--primary {
border: 4px solid orange;
backgroud-color: orange;
}

使用这种技术后,你的输出可能是这样:

1
<button class="c-button--primary is-active">Click me!</button>

我不知道使用class^=和class*= selectors的性能是否比使用独立类名好得多,但在理论上这也是一个很好的选择。我更喜欢多个类名的那种方式,但这个方式我也推荐给大家。

10. 我们可以灵活的更改组件的类型吗?

这是之前 Arie Thulank抛给我的一个问题,也是我正在努力的给出100%正确的方案的问题之一。

比如一个下拉菜单当它在某个屏幕尺寸的时候转换为一组选项卡,或者在某个触发点又变为菜单栏的屏幕外的导航(大概就是某个节点会有不同的表现形式的意思)

基本上,一个组件有两种不同的表现形式,由媒体查询【media query】决定.

对这两种特定的表现形式,我倾向于仅构建一个组件c-navigation,因为在这两个节点上,这个组件本质上也是要这么展现的。还有一种,如果在更大的屏幕上需要将一个图片的列表转换成为一个走马灯的形式展现呢?这似乎是一种边界条件对于我来说,我觉得只要有完善的文档和注释,应该可以为这种情况再创建一个独立的组件,加上一个明确的名字(如:c-image-list-to-carrousel);

Harry Roberts 写过一篇关于 响应式后缀的文章,里面提多了解决这个问题的一种方案。他的做法更多的是布局和打印风格的变化,而不是整个组件的变化,为什么不把这种技术应用于此。所以,我们可以这么来写:

1
<ul class="c-image-list@small-screen c-carousel@large-screen">

提示:你可以使用反斜杠来避开css对@符号的检查:

1
2
3
.c-image-list\@small-screen {
/* styles here */
}

我还没遇到需要创建这些组件的时候,但是如果你必须要这样要做的话,这应该是一个对开发者友好的方式。可以让后来的同事更容易的理解你的用途,我也不主张像small-screenlarge-screen-他们在这里纯粹是为了可读性。

写在最后

BEM 一直是我们模块化,组件驱动方式创建应用程序的绝对救星。我已经使用三年了,上面这些事我遇到过的坑。希望文章对您有用。