翻译计划-和BEM的战斗

发表于:August 6, 2016 at 06:00 PM

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

原文作者:@David Berner

原文链接:https://www.smashingmagazine.com/2016/06/battling-bem-extended-edition-common-problems-and-how-to-avoid-them/

译者:Icarus

邮箱: xdlrt0111@163.com

无论你是刚刚发现 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和短元素名,比如:bodytextlink,但是你可以想象当块和元素的初始部分被命名为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

这是我采用的规范,很多前缀会贯穿这篇文章的代码示例。

TYPEPREFIXEXAMPLES
Componentc-c-card c-checklist
Layout modulel-l-grid l-container
Helpersh-h-show h-hide
Statesis- has-is-visible has-loaded
JavaScript hooksjs-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.com/2016/06/battling-bem-extended-edition-common-problems-and-how-to-avoid-them/