原文链接: https://segmentfault.com/a/1190000006135647
无论你是刚刚发现BEM或者已经是个中熟手(作为web术语来说),你可能已经意识到它是一种有用的方法。如果你还不知道BEM是什么,我建议你在继续阅读这篇文章之前去BEM website了解一下它,因为我会假设你对这种CSS的方法有一个基础的理解。
本文旨在对那些已经是BEM的爱好者或是想要去更有效率的使用它或是非常好奇并且想去学习它的人有所帮助。
现在,我对BEM是一个优雅的命名方式已经不报有任何幻想。它完全不是。我曾经很长一段时间放弃接受它的原因之一就是它的语法看起来非常丑陋。我心中的设计因子不希望我优雅的html结构被丑陋的双下划线和连字符弄得一团糟。
而我心中的开发者因子让我务实地看待它。最终,这种用来构建用户界面并且有逻辑性的、模块化的方式战胜了我右半边大脑的抱怨:“但是它不够漂亮!”我当然不会建议你在像起居室这样小的范围内使用这种方式,但是当你需要一件救生衣(就像你遨游在CSS的大海中),我会选择实用而不是形式。话题拓展的差不多了,以下是10种我已经遇到过的困境和一些如何解决它们的技巧。
1. 如何使用子代甚至更深层次的选择器?
首先来解释这个问题,当你需要选择一个嵌套超过两层的元素,你就会需要用到子孙选择器。这些选择器简直就是我的梦魇,而且我很确定他们的滥用是人们对BEM产生厌恶的原因之一,看下面这个例子:
<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>
就像你想的那样,以这种方式命名会很快就会脱离控制,并且一个组件嵌套的越深,越丑陋也越不可读的类名就会出现。我已经使用了一个短块名称c-card
和短元素名,比如:body
,text
,link
,但是你可以想象当块和元素的初始部分被命名为c-drop-down-menu
会有多么失控。
我认为双下划线在选择器名称中只应该出现一次。BEM代表的是Block__Element--Modifier
,而不是Block__Element__Element--Modifier
。所以,避免多个元素级的命名。如果存在多级嵌套,你可能就需要重新审查一下你的组件结构。
BEM命名和DOM没有很严格的联系,所以无论子元素的嵌套程度有多深都没有关系。命名约定只是用来帮助你识别子元素和顶层组件块的关系,在这里就是c-card
。
这是我对相同card组件的处理:
<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块影响。所以,我们可以将文本和图片移动到c-card__header
,甚至在不破坏语义结构的情况下添加一个c-card__footer
元素。
2. 我应该使用命名空间吗?
现在,你可能已经注意到我的代码示例中使用了c-
。这代表“组件”和形成了我命名BEM类名的规范。这个想法来自于致力于提升代码可读性的Harry Robert'snamespacing technique
这是我采用的规范,很多前缀会贯穿这篇文章的代码示例。
TYPE | PREFIX | EXAMPLES |
---|---|---|
Component | c- |
c-card c-checklist |
Layout module | l- |
l-grid l-container |
Helpers | h- |
h-show h-hide |
States | is- has- |
is-visible has-loaded |
JavaScript hooks | js- |
js-tab-switcher |
我发现使用这些命名空间会使我的代码非常具有可读性。即使我不能强求你使用BEM,这也绝对是一个值得你使用的关键点。
你可以采用很多其它的命名空间,像qa-
可以用作质量保证测试,ss-
用作服务器端的钩子,等等。但是上面的列表是一个好的开始,当你觉得这项技术还不错,你可以把它介绍给其他人。
在下个问题中,会有一个比较实用的关于样式命名空间的示例。
3. 我该如何命名包裹容器?
一些组建需要一个掌控子元素布局的容器。在这种情况下,我通常会尝试把布局抽象到一个布局模块中,比如l-grid
,并且将每一个组件作为l-grid__item
的内容插入。
在我们card的示例中,如果我们想要去生成拥有四个c-card
的列表,我会使用下面的html结构:
<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>
标签移除掉的。
在一些情景下,布局模块是不可能的完全满足要求的。比如说你的网格没有能给你想要的结果,或者你只是想要去语义化的命名一个父元素,你应该怎么做?在不同的场景我倾向去选择contaniner
或者list
。还是我们card的例子,我可能会用<div class="l-cards-container">[…]</div>
or <ul class="l-cards-list">[…]</ul>
或者是<ul class="l-cards-list">[…]</ul>
,这取决于使用的条件。关键是要和你的命名约定保持一致。
4. 跨组件的组建?
我们面临的另一个常见的问题是组件的样式和位置会受到父级容器的影响。就这个问题Simurai有很多详细的解决办法。我这里说一个拓展性最好的方式。
假设我们想要在之前的示例的card__body
中加入一个c-button
。这个按钮本身已经是一个组件并且结构如下:
<button class="c-button c-button--primary">Click me!</button>
如果和常规的按钮组件没有样式差别,那么就没有问题。我们只要像下面这样直接使用:
<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
组件的一部分。也就是说,当它有一些微小的不同时我们应该怎么办?
之前我说过,我找到一个最好用的跨组件类名的解决方式。
<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> <!-- My *old* cross-component approach --> <button class="c-button c-card__c-button">Click me!</button> </div> </div>
这就是BEM官网上著名的“mix”。但是,参考了一些从Esteban Lussich的评价之后,我改变了对这种方式的看法。
在上面的例子中,c-card__c-button
类尝试去改变一个或多个c-button
中存在的属性,但是成功应用这些样式取决于他们的源顺序(或者特殊的指定)。c-card__c-button
类只会当它在源代码里声明在c-button
类之后才会生效。在你构建更多跨组件的组件时会很快失控。(当然,使用!important
也是一种选择,但是我不建议你这样做)
一个真正模块化的UI元素的父元素应该是完全不可知的-无论你在何处使用它,效果都应该是一致的。像“mix”方式那样为另一个组件添加具有特定样式的类名,违反了组件驱动设计的开/关原则,即样式在各模块之间不应该有依赖关系。
最好的办法就是在这些微小的样式差别中使用同一个类,因为你会发现随着项目的增长你会在别的地方对它进行复用。
<button class="c-button c-button--rounded c-button--small">Click me!</button>
即使你不会再使用这些类,为了应用这些修改,至少也不能把他们和父容器、特殊属性和源顺序耦合在一起。
当然,另一个选择就是回到你的设计师岗位,告诉他们这个按钮应该和网站上的其他按钮保持一致,这样就可以完全避免这个问题。但这是另一码事了。
5. 修饰器还是新组件?
决定组件的起止是一个大问题。在c-card
这个示例里,你可能之后会创建另一个叫c-panel
的组件,他们两个样式相仿,只有一些细微的差别。
但是是什么决定他们应该是两个组件呢?c-panel
和c-card
这个块名,或者仅仅是因为一个修饰器在c-card
里应用了特殊的样式。
这样很容易过度模块化并且让一切都变成一个组件。我建议从修饰器开始,但是如果你发现你特定组件的CSS文件正变得很难维护,这时候就可以停止使用修饰器。当你发现你为了适应你新的修饰器而不得不去重置这个“块”所有的CSS时,就是需要新组件的好时机-起码对我来说是这样的。
如果你和其它开发者或者设计师协作,最好的方式是去询问他们的意见并且花几分钟去讨论。我知道这样可能有点逃避责任,但是对于一个大型项目来说,理解哪些模块是可复用的并且在组件的构成上达成一致是至关重要的。
6. 如何处理状态?
这是一个常见的问题,特别是当你给一个活跃状态的组件编写样式的时候。让我们假设cards有一个活跃状态,当我们点击它时,它们会被添加上一个好看的边框。你会怎么去命名这个类?
在我看来你有两种选择:独立的状态钩或者是一个在组件级类似BEM方式命名的修饰器。
<!-- 独立状态勾 --> <div class="c-card is-active"> […] </div> <!-- BEM修饰器 --> <div class="c-card c-card--is-active"> […] </div>
尽管我更倾向于保持一致性的类似BEM的命名方式,独立类名的好处是使用JavaScript来在任意一个组件中应用一般的状态钩更容易。当你不得不使用脚本去应用特定的基于修饰器的状态类时,类BEM的方式就很让人头疼了。这当然是完全可行的,但是意味着你需要为每种可能性去编写更多的JavaScript代码。
坚持使用一系列标准的状态钩是有意义的。Chris Pearece有一个编译好的列表,我推荐你去了解一下。
7. 什么时候可以不在元素上添加类?
我可以理解很多人在需要构建一个复杂UI的时候面临的痛苦,特别是他们不习惯去在每个标签上添加一个类。
通常,我会在需要特殊样式的部分上下文添加类名。我会把p
标签级的舍弃,除非在这个组件中有特殊的需求。
可以预见,这意味着你的html中会包括非常多类名。最终,你的组件可以独立运行并且在没有副作用的条件下在任何地方使用。
由于CSS的全局特性,在所有部分都添加类让我们可以完全控制我们组件的渲染。最初的心理不适在一整个模块化的系统完成后是完全值得的。
8. 如何嵌套组件?
假设我们想要在c-card
组件中展示一个选项列表,下面这是一个反面教材:
<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
样式都被限定使用,不能复用。
我更倾向于这里需要打破在这个布局模块中的列表本身,而是应该把选项列表单独抽象成一个组件,这样就可以在其它地方独立使用它们。这里我们使用l-
命名空间。
<div class="c-card"> <div class="c-card__header"> <h2 class="c-card__title">Title text here</h3> </div> <div class="c-card__body"><div class="c-card__body"> <p>I would like to buy:</p> <!-- Much nicer - a layout module --> <ul class="l-list"> <li class="l-list__item"> <!-- A reusable nested component --> <div class="c-checkbox"> <input id="option_1" type="checkbox" name="checkbox" class="c-checkbox__input"> <label for="option_1" class="c-checkbox__label">Apples</label> </div> </li> <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> </ul> <!-- .l-list --> </div> <!-- .c-card__body --> </div> <!-- .c-card -->
这样你就不用重复哪些样式,同时也意味着我们可以在项目中的其它地方使用l-list
和c-checkbox
。可能这意味着更多的标记,但是对于可读性,封装性和可复用性来说代价可以忽略。你可能已经注意到这些是共同的主题!
9. 组件会不会最终有无数个类名?
有些人认为每个元素有大量类名是不好的,--modifiers
会越积越多。就我个人而言,我不认为这是个问题,因为这意味着代码更具有可读性,我也能更清楚的知道它是用来实现什么的。
举个例子,这是一个具有四个类的按钮:
<button class="c-button c-button--primary c-button--huge is-active">Click me!</button>
我第一眼看到的时候觉得语法不是最简洁的,但是非常清晰。
如果这让你非常头痛,你可以查看Sergey Zarouski提出的拓展技术,我们可以在样式表中使用.className [class^="className"]
和[class*=" className"]
来效仿vanilla CSS的拓展功能。如果语法看起来很眼熟,可能是因为和Icomoon处理它的icon选择器的方式非常类似。
使用这种技术,你的代码可能会看起来像下面这样:
<button class="c-button--primary-huge is-active">Click me!</button>
我不知道使用class^=
和class*=
选择器是否比独立的类名表现更好,但是理论上来说这是一个不错的选择。我喜欢使用复合类名,但我觉得这值得那些倾向于寻找替代品的人注意一下。
10. 我们可以响应式的改变组件的样式吗?
这是Arie Thulank给我提出的问题,我花费了很多心思去想出一个100%具体具体的解决办法。
一个例子就是下拉菜单在给定断点处转换为选项卡或者是隐式导航在给定断点处转换为菜单栏。本质上是一个组件在媒体查询的控制下有两种不同的样式表现。
我倾向于给这两个例子去构建一个c-navigation
组件,因为他们在两个断点处的行为本质是相同的。但这让我陷入沉思,如果是图片列表在大屏幕上转化为轮播图呢?这对我来说是一个边缘情况,只要它有可行的文档及评论,我认为这是合理的。可是使用明确的命名(像c-image-list-to-carousel
)来为这种类型的UI构造一次性的独立组件。
Harry Roberts写过一篇响应式后缀来解决这个问题。他的做法是为了适应更多布局和样式的变化,而不是整个组件的变化。但我不明白为什么这项技术不能被应用在这里。所以,你会发现作者写的样式像下面这样:
<ul class="c-image-list@small-screen c-carousel@large-screen">
对于不同的屏幕尺寸,这些类就会保留各自的媒体查询。提示:在CSS中你需要在@
前加上来进行转义,像下面这样:
.c-image-list@small-screen { /* styles here */ }
我没有太多构造这种组件的示例,但是如果你需要构造这种组件,这将是一个对开发者非常友好的方式。下一个加入的人应该可以轻松理解你的想法。我不提倡你使用像small-screen
和large-screen
这样的命名,他们只是单纯为了可读性。
本文根据@David Berner的《Battling BEM (Extended Edition): 10 Common Problems And How To Avoid Them》所译,整个译文带有我自己的理解与思想,如果译得不好或有不对之处还请多多指点。如需转载此译文,需注明英文出处:https://www.smashingmagazine....