如何更好的使用BEM
BEM是由Yandex团队提出的一种CSS Class 命名方法,旨在更好的创建CSS/Sass模块。他需要遵循一些特殊规定,有些人认为这些规定很冗余,但是我发现他们对于理解DOM有着很大的帮助。你可以去查看我之前的文章去了解为什么BEM如此伟大。或者你可以去查看这几篇中文文章来了解BEM(《BEM的定义》《为什么我们需要BEM》)。今天这篇文章,是我在假设你对BEM和Sass已经有所了解甚至熟悉的基础上完成的。
BEM又丑又傻?
许多人认为BEM是一种多余的,丑陋的长命名。我完全能够理解当你在使用一种臃肿的和过度描述的命名方式时会发出这样的抱怨。这种情况下你会发现Sass编译后的CSS会非常的冗余和丑陋,尤其是当你在很长的BEM命名内又嵌套了更长的BEM命名时。这简直是非常糟糕和令人沮丧的一件事。
.aboutSection {
background-color: tomato;
}
.aboutSection__wrapper {
max-width: 108rem;
padding: 3rem 0;
}
.aboutSection__headingContainer {
background-color steelblue;
.aboutSection__header {
font-size: 2.4rem;
font-weight: 700;
}
.aboutSection__subHeader {
font-size: 1.8rem;
color: green;
}
}
可能很多刚开始使用BEM的程序员会写出像上面一样的CSS/Scss代码。虽然手指已经很酸痛了但是还是每次都兢兢业业地输入完整的类名。我完全能够理解这时你会感觉BEM是一种很傻很多余的命名规则。我们在不断的重复自己来为不同的类名设置不同的样式,而且不具有可重复性。
在Sass中使用BEM
灵活的&__
通过使用一组便利的选择器和运算符,我们可以使得BEM出类拔萃。其中最重要的是新加入Sass中的&
选择器.在声明内&
选择器会直接引用他的父选择器。当嵌套使用'&'时,它会直接抓取父选择器的类名。因为我们的类名遵循真正可靠的模式,所以我们可以把它作为我们的优势来打造真正可读的代码。
.aboutSection {
&__wrapper {
max-width 108rem;
padding: 3rem 0;
}
&__headingContainer {
background-color: steelblue;
}
&__header {
font-size: 2.4rem;
font-weight: 700;
}
&__subHeader {
font-size: 1.8rem;
color: green;
}
}
以上这段代码就是一个利用&
选择器的BEM创建的Sass模块,这个模块看上去像是嵌套但并不会带来嵌套所造成的权重差异等问题。这将有效帮助你避免CSS文件中组件样式的冲突。同时采用Sass的嵌套选择器也使我们的代码更加直观可读。
强大的@extend
我们可以进一步使用@extend
来改进我们的BEM。BEM允许我们在类的最后添加一个修饰符,当以纯CSS编写样式时,就需要你给同一个元素两个类的标记,比如:
<navclass="nav">
<ulclass="nav__container">
<liclass="nav__item"></li>
<liclass="nav__item"></li>
<liclass="nav__item"></li>
<liclass="nav__item nav__item--active"></li>
</ul>
</nav>
以上这种情况是令人很沮丧的,为什么我们需要添加一个又长又绕的类来说明这个元素是active
的?为什么我们不能仅仅增加一个名为active
的类?通过使用@extend
指令,我们就可以很好地消除这种冗余。
.nav {
background-
color: steelblue;
&__container {
display: flex;
justify-
content: space-between;
}
&__item {
color: white;
&--active {
@extend .nav__item;
border-
bottom:1px solid red;
}
}
}
@extend
指令能让我们确保所选择的项目也具有nav_item
类中的所有样式,从而清除HTML中的nav_item
标记。使用这种方法,我们能够随心所欲地改变活动状态的颜色,而无需重写我们之前创建的样式。在我看来,一个元素应该只有一个类,我们应该活用Sass来达到我们想要的结果。
结束语
我希望这篇文章能够使你相信尽管BEM的好处远远超过它的那点瑕疵。也许有人会告诉你相反的结果,也许在一开始你会觉得它很冗余,但是通过BEM,我们CSS文件中所有选择器都保持相似的特异性水平。当我们使用'&'选择器时我们可以在保持有很好的组织性和可读性的同时避免嵌套带来的样式冲突。通过使用@extend
指令,我们不仅可以消除BEM命名的冗余,而且可以修剪下来你的标记。
本文根据@GarrettLevine的《Getting Sass-y with BEM》所译,整个译文带有我们自己的理解与思想
如需转载,烦请注明出处:http://www.w3cplus.com/preprocessor/getting-sass-y-with-bem.html
著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。
原文: http://www.w3cplus.com/preprocessor/getting-sass-y-with-bem.html © w3cplus.com
关于BEM中常见的十个问题以及如何避免
不管你是刚刚才接触BEM还是已经是一名老手,对于它的思想你是不是都十分的赞美?如果你还没有接触过BEM,在阅读这篇文章之前我建议你先到BEM官方网站进行了解,因为我将对其进行分类表述我对这种CSS理念的观点。
本文旨在对那些BEM的爱好者以及想要更好地使用它或者是想要加深对它了解的人有所帮助。
现在我是不抱有任何幻想,关于说BEM是一个很好的CSS命名规范。因为它绝对不是!我曾经很长一段时间因为它丑陋的语法规范而放弃了它。作为设计者的我不希望我的标记中出现丑陋的双下划线以及连字符。
但是作为构建一个逻辑性的、模块化的用户界面,它是比较务实的,这胜过了我的右大脑的抱怨 - “但是它不够漂亮!!!”。我当然不推荐在起居室这种小范围内使用这种方式,但是当你需要一件救生衣(当你遨游在CSS大海之中时),我将会选择这种函数形式。不管怎么说,足够使用。这里有10个关于BEM的问题以及我处理这些问题的小技巧。
关于后代(不包含子选择器)选择器如何使用?
这里需要澄清一下,当你的元素嵌套两级以及大于两级的时候就需要使用子孙选择器。这简直就是我生命中的克星,我敢肯定他们的滥用就是有人对BEM产生反感的原因之一,如下所示:
<divclass="c-card">
<divclass="c-card__header">
<!-- Here comes the grandchild… -->
<h2class="c-card__header__title">Title text here
</h2>
</div>
<divclass="c-card__body">
<imgclass="c-card__body__img"src="some-img.png"alt="description">
<pclass="c-card__body__text">Lorem ipsum dolor sit amet, consectetur
</p>
<pclass="c-card__body__text">Adipiscing elit.
<ahref="/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
。
这里我就会这样子处理相同的卡片组件,如下:
<divclass="c-card">
<divclass="c-card__header">
<h2class="c-card__title">Title text here
</h2>
</div>
<divclass="c-card__body">
<imgclass="c-card__img"src="some-img.png"alt="description">
<pclass="c-card__text">Lorem ipsum dolor sit amet, consectetur
</p>
<pclass="c-card__text">Adipiscing elit.
<ahref="/somelink.html"class="c-card__link">Pellentesque amet
</a>
</p>
</div>
</div>
这意味着所有的后代元素仅仅受到card
块的影响。因此我们可以将文本和图像移动到c-card__header
,甚至引入新的c-card__footer
元素而不会破坏语义结构。
如何使用命名空间?
现在你可能已经注意到我在上述代码中使用 c-
,这表示“组件”,并且形成了我对BEM类命名规范。这个想法来自于Harry Robert的命名技巧,提高了代码的可读性。
我发现使用这些命名空间使我的代码更具有可读性,即使我不使用BEM,这也是一个重要的分辨点。
你也可以采用其余的命名空间,如qa-
表示质量保证,ss-
表示服务器端挂钩等。但是上述列表是一个很好的参照,如果你感觉还不错可以将其介绍给别人。
你会发现这种风格在未来命名空间问题上会是一个很好的例子。
我该如何命名包裹容器?
一些部件需要一个决定子元素布局的父容器。在这些情况下,我总是尝试将布局抽象为一个布局模块,如l-grid
,并将其组件插入其中,如l-grid__item
。
在我们的卡片示例中,如果我们想要生成一个具有四个c-card
的列表,我将会使用下面所示的标记:
<ulclass="l-grid">
<liclass="l-grid__item">
<divclass="c-card">
<divclass="c-card__header">
[…]
</div>
<divclass="c-card__body">
[…]
</div>
</div>
</li>
<liclass="l-grid__item">
<divclass="c-card">
<divclass="c-card__header">
[…]
</div>
<divclass="c-card__body">
[…]
</div>
</div>
</li>
<liclass="l-grid__item">
<divclass="c-card">
<divclass="c-card__header">
[…]
</div>
<divclass="c-card__body">
[…]
</div>
</div>
</li>
<liclass="l-grid__item">
<divclass="c-card">
<divclass="c-card__header">
[…]
</div>
<divclass="c-card__body">
[…]
</div>
</div>
</li>
</ul>
现在你应该对布局模块和组件的命名空间的组合使用有了一个坚实的想法。
不要害怕使用一些额外的标记需要记忆而头疼。没有人会拍拍你的肩膀让你移除<div>
标签的!
在一些情况下,这是不可能的。如,你的网格不能实现你想要的结果,或者说你想要一些语义化对父元素进行命名,这时你要怎么办?对于我,根据不同场景,我倾向于选择使用container
或者list
。返回我们的卡片示例,我可能会使用<divclass="l-cards-container">[…]</div>
或者<ulclass="l-cards-list">[…]</ul>
。关键点在于和你的命名约定相一致。
跨组件...组件?
所面临的另外一个问题是,组件的风格或者定位受其父容器的影响。关于这个问题Simurai对各种解决方案做了详细的说明。这里我就告诉大家我认为最具有扩展性的解决方法。
假设我们想要我们的示例中card__body
中插入一个c-button
。这个按钮已经有自己的组件,并且标记如下:
<buttonclass="c-button c-button--primary">Click me!
</button>
如果与其它常规按钮组件没有样式区别,这里就不存在问题,我们可以如下所示直接使用:
<divclass="c-card">
<divclass="c-card__header">
<h2class="c-card__title">Title text here
</h3>
</div>
<divclass="c-card__body">
<imgclass="c-card__img"src="some-img.png">
<pclass="c-card__text">Lorem ipsum dolor sit amet, consectetur
</p>
<pclass="c-card__text">Adipiscing elit. Pellentesque.
</p>
<!-- Our nested button component -->
<buttonclass="c-button c-button--primary">Click me!
</button>
</div>
</div>
但是,当其样式不同时如,我们想要其更小一点,或者完全是圆角,但是这些情况仅仅出现在c-card
组件的一部分。这时应该怎么办?
之前我说过,我找到一个跨组件最强大的解决方案:
<divclass="c-card">
<divclass="c-card__header">
<h2class="c-card__title">Title text here
</h3>
</div>
<divclass="c-card__body">
<imgclass="c-card__img"src="some-img.png">
<pclass="c-card__text">Lorem ipsum dolor sit amet, consectetur
</p>
<pclass="c-card__text">Adipiscing elit. Pellentesque.
</p>
<!-- My *old* cross-component approach -->
<buttonclass="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"方法,违反了组件导向设计的开/关原则 - 即各个模块之间应该没有依赖关系。
最好的解决办法就是在这些不同之处使用一个修饰器,因为你可能会发现,你会在其它地方对其复用。
<buttonclass="c-button c-button--rounded c-button--small">Click me!
</button>
即使你不会再次使用这些类,在修饰器指定特异性或者源顺序,它们不会被捆绑到父容器上。
当然,另外一个选择就是回到你的设计者岗位,告诉他们按钮的样式就应该和网站上其余按钮保持一致,并完全避免这个问题......但是,这又是另一天。
修饰器或新建组件?
最大的问题之一在于决定组件的结束与另一个组件的开始。在c-card
示例中,你可能随后就会创建一个c-panel
组件,它们之间有很大的相似之处,但是又有细微的差别。
但是,你需要决定是否应该存在有两个组件 - c-panel
和c-card
,或者在c-card
上应用一个简单的修饰器对样式进行修改。
这很容易造成过渡模块化,使其一切变为组件。我建议使用修饰器,但是如果你发现你的特定组件的CSS文件越来越难以管理,这时你就可以终止对修饰器的使用。一个很好的指标就是,当你发现你不得不重置你所有的块CSS以对你的修饰器进行样式修饰时 - 这对我来说就可以生成一个新组件。
最好的办法就是,如果你和其余的开发者或者设计师一起工作,征求他们的意见并进行讨论。我知道这有点虎头蛇尾,但是对于一个大型应用程序,明白模块化的复用性以及什么时候需要组件是至关重要的。
如何处理状态?
这是一个普遍的问题,特别是当你对组件的活动或开启状态进行样式修饰时。比方说我们的卡片需要有一个活跃状态;所以当你点击时,就会显现一个漂亮的边框样式。这里你将如何对类进行命名呢?
这里我感觉你会有两个选择: 一个是独立的状态钩,或者是在组件中添加一个BEM中的修饰符:
<!-- standalone state hook -->
<divclass="c-card is-active">
[…]
</div>
<!-- or BEM modifier -->
<divclass="c-card c-card--is-active">
[…]
</div>
这里我喜欢保持一致性,使用BEM类似命名。一个独立类的好处就是,它可以很容易的使用JavaScript在所有组件上应用通用状态的挂钩。当你不得不用脚本应用特定的基于修饰器的状态类时,这就会变得有些困难。当然,这是完全有可能的,但意味着使用了大量的JavaScript脚本。
坚持使用一套标准挂钩是有道理的。ChrisPearce 有一个已经编译好的目录,我建议阅读一下。
什么时候可以不用在一个元素上添加类?
我可以理解人们对于构建一个复杂的UI界面所面临的大量的类的苦楚,特别是他们不向每个类上添加一个标签。
通常情况下,我会在有特殊样式的组件上下文中添加类。我经常丢弃p
标签级的,除非是组件的特殊需要。
当然这可能意味着你的标记中包含大量的类,但是你的组件可以独立化并且可以在任何地方无副作用使用。
由于CSS的全局特性,应用样式所控制的远远大于组件所渲染的样式。这种最初的心理不适性导致是值得的,相对于模块化系统所带来的好处。
如何嵌套组件?
假设我们想在我们的c-card
组件中展示一个选择清单,这里有一个错误的示范:
<divclass="c-card">
<divclass="c-card__header">
<h2class="c-card__title">Title text here
</h3>
</div>
<divclass="c-card__body">
<p>I would like to buy:
</p>
<!-- Uh oh! A nested component -->
<ulclass="c-card__checklist">
<liclass="c-card__checklist__item">
<inputid="option_1"type="checkbox"name="checkbox"class="c-card__checklist__input">
<labelfor="option_1"class="c-card__checklist__label">Apples
</label>
</li>
<liclass="c-card__checklist__item">
<inputid="option_2"type="checkbox"name="checkbox"class="c-card__checklist__input">
<labelfor="option_2"class="c-card__checklist__label">Pears
</label>
</li>
</ul>
</div>
<!-- .c-card__body -->
</div>
<!-- .c-card -->
这里存在几个问题。其中一个就是我们在section1
部分使用的祖父母选择器。另外一个问题就是所有应用c-card__checklist__item
类的均被限定使用,不可复用。
这里我更倾向于将清单本身变为一个布局模块并且清单子项目将会成为它的组件,使他们能够在其他地方独立使用。这里使用l-
命名空间:
<divclass="c-card">
<divclass="c-card__header">
<h2class="c-card__title">Title text here
</h3>
</div>
<divclass="c-card__body"><divclass="c-card__body">
<p>I would like to buy:
</p>
<!-- Much nicer - a layout module -->
<ulclass="l-list">
<liclass="l-list__item">
<!-- A reusable nested component -->
<divclass="c-checkbox">
<inputid="option_1"type="checkbox"name="checkbox"class="c-checkbox__input">
<labelfor="option_1"class="c-checkbox__label">Apples
</label>
</div>
</li>
<liclass="l-list__item">
<divclass="c-checkbox">
<inputid="option_2"type="checkbox"name="checkbox"class="c-checkbox__input">
<labelfor="option_2"class="c-checkbox__label">Pears
</label>
</div>
</li>
</ul>
<!-- .l-list -->
</div>
<!-- .c-card__body -->
</div>
<!-- .c-card -->
这样你就不用重复样式,也意味着可以在应用的其它地方应用l-list
类和c-checkbox
类。这意味着多一点标记,但是方便了阅读,封装以及可重用性。你可能已经注意到了这都是共同的话题!
组件是不是结束于大量类之中?
一些人认为,在一个元素上应用大量的类是很不好的,而且--modifiers
会积少成多。就我个人而言,我不感觉这有问题,因为这使代码更具有可读性,可以很快明白其语义。
这里有一个应用了四个类的按钮:
<buttonclass="c-button c-button--primary c-button--huge is-active">Click me!
</button>
我知道这个语法不是最可视化的,但是却是最明确的。
但是,如果这对于你来说是一个难题,你可以看看Sergey Zarouski所提出的扩展技术。从本质上说,我们将会在样式表中使用.className[class^="className"],[class*=" className"]
效仿vanilla CSS的扩展功能。如果你对这种语法比较眼熟,这是因为它和Icomoon处理图标选择是十分相似的。
使用这种方式,你的标记可能会如下所示:
<buttonclass="c-button--primary-huge is-active">Click me!
</button>
我不知道使用class=^
和class*=
选择器所带来的性能损失是否比单独使用大规模的类大得多,但是这在理论上是一个很好的替代。我对于多类的选择还是可以接受的,但是我感觉应该为那些喜欢选择的人提及一下。
是否可使组件具有响应化?
这个问题是Arie Thulank向我提出的,也是一个我挣扎了好久想出一个100%具体解决方案的问题。
一个例子就是在给定断点一组选项卡的转换,或屏幕导航在给定断点处切换为下拉菜单样式。
从本质上讲,一个组件在媒体查询下具有两个不同的表现形式。
关于这两个具体例子,我的倾向是建立一个c-navigation
组件,因为在两个断点处它的行为是最基础的。这让我产生了一个思考,在大屏幕上如何将一个图像列表转换为幻灯片形式?这对于我来说可能是一个边缘情况,并且只要是具有可行性文档和评论,我认为这是完全合理的,为这种类型的用户界面创建这种具有明确的命名(如c-image-list-to-carousel
)一次性独立组件。
Harry Roberts曾写过响应后缀 ,这也是一种处理方式。他的做法是为了更多的布局和样式的变化,而不是整个组件的变化,但我不明白为什么这种技术不能应用在这里。所以,基本上你会发现作者的类是这样的:
<ulclass="c-image-list@small-screen c-carousel@large-screen">
这样,对于不同的屏幕尺寸就会保留各自的媒体查询。 提示:你需要在你的CSS中使用反斜杠使用@
,如下所示:
.c-image-list\
@small-screen {
/* styles here */
}
我没有太多的示例创建这些类型的组件,但是你必须这样子做,这感觉将会是一种十分有好的开发商方式。下面接管你代码的人应该也能够很容易地理解你的意图。 我不主张像small-screen
和large-screen
的命名 - 这里纯粹是为了可读性。
总结
BEM绝对是我在创建一个具有模块化,组件驱动方式的应用中的一个救星。现在我已经使用了近三年,上述问题是也是我遇到的为数不多的绊脚石。我希望这篇文章对你的学习有所帮助,另外如果BEM不过时,我强烈建议你这样做。
著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。
原文: http://www.w3cplus.com/css/battling-bem-extended-edition-common-problems-and-how-to-avoid-them.html © w3cplus.com