和BEM的战斗:10个常见问题及如何避免

不管你是刚刚发现BEM或者已是个中熟手(做为web术语来讲),你可能已经意识到它是一种有用的方法。若是你还不知道BEM是什么,我建议你在继续阅读这篇文章以前去BEM website了解一下它,由于我会假设你对这种CSS的方法有一个基础的理解。css

本文旨在对那些已是BEM的爱好者或是想要去更有效率的使用它或是很是好奇而且想去学习它的人有所帮助。html

如今,我对BEM是一个优雅的命名方式已经不报有任何幻想。它彻底不是。我曾经很长一段时间放弃接受它的缘由之一就是它的语法看起来很是丑陋。我心中的设计因子不但愿我优雅的html结构被丑陋的双下划线和连字符弄得一团糟。git

而我心中的开发者因子让我务实地看待它。最终,这种用来构建用户界面而且有逻辑性的、模块化的方式打败了我右半边大脑的抱怨:“可是它不够漂亮!”我固然不会建议你在像起居室这样小的范围内使用这种方式,可是当你须要一件救生衣(就像你遨游在CSS的大海中),我会选择实用而不是形式。话题拓展的差很少了,如下是10种我已经遇到过的困境和一些如何解决它们的技巧。github

1. 如何使用子代甚至更深层次的选择器?

首先来解释这个问题,当你须要选择一个嵌套超过两层的元素,你就会须要用到子孙选择器。这些选择器简直就是个人梦魇,并且我很肯定他们的滥用是人们对BEM产生厌恶的缘由之一,看下面这个例子:web

<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和短元素名,好比:bodytextlink,可是你能够想象当块和元素的初始部分被命名为c-drop-down-menu会有多么失控。服务器

我认为双下划线在选择器名称中只应该出现一次。BEM表明的是Block__Element--Modifier,而不是Block__Element__Element--Modifier。因此,避免多个元素级的命名。若是存在多级嵌套,你可能就须要从新审查一下你的组件结构。app

BEM命名和DOM没有很严格的联系,因此不管子元素的嵌套程度有多深都没有关系。命名约定只是用来帮助你识别子元素和顶层组件块的关系,在这里就是c-cardide

这是我对相同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-panelc-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-listc-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-screenlarge-screen这样的命名,他们只是单纯为了可读性。

总结

BEM在我建立一个模块化和组件驱动的应用时帮了大忙。我已经使用它大概有三年了,上面的这些问题是我在探索时遇到的阻碍。我但愿你认为这篇文章是有用的。若是你尚未想要体验BEM,我很是鼓励你去尝试一下。

本文根据@David Berner的《Battling BEM (Extended Edition): 10 Common Problems And How To Avoid Them》所译,整个译文带有我本身的理解与思想,若是译得很差或有不对之处还请多多指点。如需转载此译文,需注明英文出处:https://www.smashingmagazine....

相关文章
相关标签/搜索