BEM是由Yandex公司推出的一套CSS命名规范,官方是这么描述它的:
BEM是一种让你可以快速开发网站并对此进行多年维护的技术。
一开始,Yandex公司推出的BEM,包括了规范以及其配套构建工具。如今提到的BEM主要是指其中的规范,在BEM最新的推广页中,对其的描述为:
BEM是一种命名方法,能够帮助你在前端开发中实现可复用的组件和代码共享。
什么是BEM?
BEM代表块(Block),元素(Element),修饰符(Modifier)。这些术语的含意将在本文进一步阐述。
编程方法论中一个最常见的例子就是面向对象编程(OOP)。这一编程范例出现在许多语言中。在某种程度上,BEM和OOP是相似的。它是一种用代码和一系列模式来描述现实情况的方法,它只考虑程序实体而无所谓使用什么编程语言。
我们使用BEM的原则创建了一个前端开发技巧和工具的集合,这样我们就能快速构建一个网站,并且保证他们长久的可维护性。
BEM解决的问题
css的样式应用是全局性的,没有作用域可言。考虑以下场景
场景一:开发一个弹窗组件,在现有页面中测试都没问题,一段时间后,新需求新页面,该页面一打开这个弹窗组件,页面中样式都变样了,一查问题,原来是弹窗组件和该页面的样式相互覆盖了,接下来就是修改覆盖样式的选择器...又一段时间,又开发新页面,每次为元素命名都心惊胆战,求神拜佛,没写一条样式,F5都按多几次,每个组件都测试一遍...
场景二:承接上文,由于页面和弹窗样式冲突了,所以把页面的冲突样式的选择器加上一些结构逻辑,比如子选择器、标签选择器,借此让选择器独一无二。一段时间后,新同事接手跟进需求,对样式进行修改,由于选择器是一连串的结构逻辑,看不过来,嫌麻烦,就干脆在样式文件最后用另一套选择器,加上了覆盖样式...接下来又有新的需求...最后的结果,一个元素对应多套样式,遍布整个样式文件...
以往开发组件,我们都用“重名概率小”或者干脆起个“当时认为是独一无二的名字”来保证样式不冲突,这是不可靠的。
理想的状态下,我们开发一套组件的过程中,我们应该可以随意的为其中元素进行命名,而不必担心它是否与组件以外的样式发生冲突。
BEM解决这一问题的思路在于,由于项目开发中,每个组件都是唯一无二的,其名字也是独一无二的,组件内部元素的名字都加上组件名,并用元素的名字作为选择器,自然组件内的样式就不会与组件外的样式冲突了。
这是通过组件名的唯一性来保证选择器的唯一性,从而保证样式不会污染到组件外。
这也可以看作是一种“硬性约束”,因为一般来说,我们的组件会放置在同一目录下,那么操作系统中,同一目录下文件名必须唯一,这一点也就确保了组件之间不会冲突。
BEM的命名规矩很容易记:block-name__element-name--modifier-name,也就是模块名 + 元素名 + 修饰器名。
想象一个如下图所示的普通网站。
就这样的一个网站来说,指出它是由哪些“块”构成的,对开发是很有帮助的。
例如,在上图中有Head,Main Layout和Foot块。Head由Logo,Search,Auth块和Menu组成。MainLayout包括一个Page Title和一个Text块。
对于团队沟通来说给页面的每一部分起个名字是很有用的。
这样的话一个项目经理就可以这么说:
让Head再大点;
创建一个Head中不带Search的页面。
一个HTML开发人员可以对一个JavaScript开发人员说:
给Auth块来点动画,等等
现在让我们仔细了解一下什么是BEM:
一个块是一个独立的实体,就像应用的一块“积木”。一个块既可以是简单的也可以是复合的(包含其他块)。
例如搜索表单块:
一个元素是块的一部分,具有某种功能。元素是依赖上下文的:它们只有处于他们应该属于的块的上下文中时才是有意义的。
例如一个输入域和一个按钮是Search块的中的元素。
块和元素构成了页面的内容。它们不仅仅是被呈现在一个页面上,它们的排列顺序也同样重要。
块(或元素)彼此之间可能遵循着某种顺序。
例如,电商网站上的一个商品列表:
……或者菜单项:
块里也有可能再嵌套块。
例如,一个Head块会包含其他块:
除了我们自己创建的一些块之外 ,我们还需要一种途径来描述页面上那些纯文本的布局。为此,每一个块和元素都应该有一个可以识别的关键字。
用来标识一个具体块的关键字其实就是这个块的名字(block name)。
例如,menu可以作为Menu块的关键字,head可以作为Head块的关键字。
用来标识一个元素的关键字也是这个元素的名字。
例如,菜单中的每个菜单项就是menu块的item元素。
一个项目中的块名必须是唯一的,明确指出它所描述的是哪个块。相同块的实例可以有相同的名字。在这种情况下一个块在一个页面上可以出现2(3,4,……)次。
一个块范围内的一种元素的名字也必须是唯一的。一种元素可以重复出现多次。
例如,菜单项:
关键字应该按一定的顺序摆放。任何支持嵌套的数据格式(XML,JSON)都可以:
<b:page> <b:head> <b:menu> … </b:menu> <e:column> <b:logo/> </e:column> <e:column> <b:search> <e:input/> <e:button>Search</e:button> </b:search> </e:column> <e:column> <b:auth> … </b:auth> <e:column> </b:head></b:page>
在这个例子中,b和e两个命名空间用来区分块和元素两种节点。
同样可以用JSON格式来表示:
{ block: 'page', content: { block: 'head', content: [ { block: 'menu', content: … }, { elem: 'column', content: { block: 'logo' } }, { elem: 'column', content: [ { block: 'search', content: [ { elem: 'input' }, { elem: 'button', content: 'Search' } ] } ] }, { elem: 'column', content: { block: 'auth', content: … } } ] } }
上面的例子展示了一个块和元素相互嵌套的对象模型。这个结构中也可以包含任意多的自定义数据字段。
我们把这种结构叫BEM树(和DOM树类似)。
通过把模板转换(使用XSL或是JavaScript)应用到BEM树上生成最终的浏览器标签。
如果开发人员需要把一个块移动到页面的其他地方,他只需要改变一下BEM树就好了。模板会生成最终的视图。
你可以使用任何格式来描述BEM树,也可以使用任何模板引擎。
我们把JSON作为页面的描述格式。然后通过一个基于JS的模板引擎BEMHTML来把它转换为HTML。
随着一个项目的发展,我们常常会在页面中添加,删除,或者是移动一些块。例如,你可能想要互换Logo和Auth块,或者把Menu放到Search块下面。
为了让这个过程更加简化,块必须是独立的。
一个独立的块可以放置在页面的任意位置 ,包括嵌套在其他块里。
从CSS的角度来看:
一个块(或者一个元素)必须有一个唯一的“名字”(一个CSS类)这样才能被CSS规则所作用。
HTML元素不能用作CSS选择器(如.menu td)因为这样的选择器并非是完全上下文无关的。
避免使用级联(cascading)选择器(译注:如.menu .item)。
下面是一种可能的CSS类命名方案:
一个块的CSS类名就是这个块的名字(block name)。
<ul class="menu">...</ul>
一个元素的CSS类名是一个块名和一个元素名的组合,它们中间用一些符号隔开。
<ul class="menu"> <li class="menu__item">…</li> <li class="menu__item">…</li></ul>
在一个元素的CSS类名中包含一个块名是必要的,这样可以让级联最小化。
我们在长名称中使用连字符分隔单词(例如,block-name),使用两个下划线来分隔块名和元素名(block-name__element-name)。
例如:
block-name--element-name
blockName-elementName
对于模板引擎来说,块的独立性意味着:
输入的数据要描述清楚块和元素:块(或元素)必须有个唯一的“名字”,在模板中告诉我们诸如“Menu应该放到这里”这样的事情。
块可以出现在BEM树的任意地方。
当模板引擎在某个模板中遇到一个块的时候可以准确地把这个块转换成HTML。因此每个块都应该有自己的模板。
例如,下面就是一个XSL模板:
<xsl:template match="b:menu"> <ul class="menu"> <xsl:apply-templates/> </ul></xsl:template><xsl:template match="b:menu/e:item"> <li class="menu__item"> <xsl:apply-templates/> </li><xsl:template>
我们的产品正在逐渐弃用XSLT,我们用我们自己开发的基于JavaScript的模板引擎XJST来解析它。这个模板引擎吸收了所有XSLT的优点(我们是声明性编程的粉丝),并且在服务端和客户端都可以用JavaScript来实现。
现在我们用一种叫做BEMHTML的特定领域语言来编写我们的模板,它是基于XJST的。BEMHTML的主要思想在Ya.Ru(在俄罗斯)的BEM俱乐部里提出。
一个网站的第二个Menu块可能出现在Foot块里。或者,一个Text会变成两个,中间被一个广告分开。
即使一个块被开发成单独的单元,那么相同的块也可能在任何时候出现在这个页面上。
在CSS相关的术语中,这意味着:
一定不能使用ID选择器:只有类选择器才能满足我们非唯一性的需求
在JavaScript里意味着:
可以准确地检测到具有相同行为的块,因为它们有相同的CSS类名:使用CSS类选择器,可以拣选出所有相同名字的块,方便给它们定义动态行为
我们经常需要创建一个和已存在的块非常相似的块,只是外观或行为有些许改变。
比如说我们有一个这样的任务:
给Footer添加另外一个布局不一样的Menu。
为了避免开发一个和现有的块只稍微有点不同的另一个块,我们引入修饰符(modifier)的概念。
修饰符作为一个块或是一个元素的一种属性,代表这个块或这个元素在外观或是行为上的改变。
一个修饰符有一个名字和一个值。多个修饰符可以同时使用。
例如:一个用来指定背景颜色的块修饰符
例如:一个改变“当前”选项的元素修饰符
在BEM树里,修饰符是用来描述一个块或者是一个元素实体的属性的。
例如,它们可以作为XML的属性节点:
<b:menu m:size="big" m:type="buttons"> …</b:menu>
用JSON也可以:
{ block: 'menu', mods: [ { size: 'big' }, { type: 'buttons' } ] }
对于一个块或者是一个元素来说修饰符是一个附加的CSS类。
<ul class="menu menu_size_big menu_type_buttons">…</ul>
.menu_size_big { // CSS code to specify height} .menu_type_buttons .menu__item { // CSS code to change item's look}
我们用一个下划线来分隔块名(或元素名)和修饰符名,再用另一个下划线来分隔修饰符名和它对应的值。(译注:此处原文是俄文,大致就是这个意思)。
元素修饰符以相同的方式实现。
当手写CSS代码的时候,为了编程的可访问性,始终使用分隔符非常重要。
例如,当前菜单项就可能被一个修饰符标记:
<b:menu> <e:item>Index<e:item> <e:item m:state="current">Products</e:item> <e:item>Contact<e:item></b:menu>
{ block: 'menu', content: [ { elem: 'item', content: 'Index' }, { elem: 'item', mods: { 'state' : 'current' }, content: 'Products' }, { elem: 'item', content: 'Contact' } ] }
.menu__item_state_current{ font-weight: bold;}
上面的结构在HTML中代表如下:
<ul class="menu"> <li class="menu__item">Index</li> <li class="menu__item menu__item_state_current">Products</li> <li class="menu__item">Contact</li></ul>
或使菜单类独立于它布局的实现细节之外:
<div class="menu"> <ul class="menu__layout"> <li class="menu__layout-unit"> <div class="menu__item">Index</div> </li> <li class="menu__layout-unit"> <div class="menu__item menu__item_state_current">Products</div> </li> <li class="menu__layout-unit"> <div class="menu__item">Contact</div> </li> </ul></div><div class="menu"> <table class="menu__layout"> <tr> <td class="menu__layout-unit"> <div class="menu__item">Index</div> </td> <td class="menu__layout-unit"> <div class="menu__item menu__item_state_current">Products</div> </td> <td class="menu__layout-unit"> <div class="menu__item">Contact</div> </td> </tr> </table></div>
当许多人开发同一个项目他们应该在数据域上保持一致,在给块和元素命名的时候用上它。例如,一个标签云的块始终命名为tags。其中的每个元素就是一个tag。这种约定贯穿所有的语言:CSS,JavaScript,XSL等等。
从开发处理的角度来看:所有参与都基于同样的约定 。
从CSS角度开看:• 块和元素的CSS可以写成一种伪代码,然后根据命名约定编译成CSS。
.menu { __layout { display: inline; } __layout-item { display: inline-block; … } __item { _state_current { font-weight: bold; } } }
相应JavaScript:可以使用专门的辅助库来替代使用类名直接选择DOM元素:
$('menu__item').click( … );$('menu__item').addClass('menu__item_state_current');$('menu').toggle('menu_size_big').toggle('menu_size_small');
块和元素的CSS类命名约定可能在一段时间内发生改变。如果命名约定改变了,只需要修改那些要访问块和元素,以及可能处理它们的修饰符的方法(function)。
block('menu').elem('item').click( … );block('menu').elem('item').setMod('state', 'current');block('menu').toggleMod('size', 'big', 'small');
上面的代码是抽象的,现实中我们使用bem-bl块中的i-bem块的JavaScript核心和库。
一个网站有一个带有某种动态行为的Button块。
当鼠标滑过这样的块,它的外观就会改变。
经理可能会要求在别的页面也使用相同的按钮。
一个块光有CSS实现还是不够的。复用一个块也意味着复用它的行为,也就是它所绑定的JavaScript。
所以一个块必须“知道”关于它自己的一切。为了实现一个块,要用我们所用到的所有技术来描述清楚它的外观和行为,我们把这叫多语言机制。
多语言描述一个块的时候涉及实现这个块的外观和功能的所有编程语言(技术)。
想让一个块像一个UI元素一样展现在页面上,我们需要用下面这些技术来实现它:
模板(XSL,TT2,JavaScript等等),把块的声明转换成HTML代码
CSS,描述块的外观
JavaScript,如果块有动态行为的话
图片
文档
构成一个块的每样东西都可以作为一种块技术。
Yandex是一个大公司(在俄罗斯),它使用BEM方法论来开发它的服务。
BEM方法论并不要求你使用某种框架。你也不必在你用到的所有页面技术中都使用BEM(但是那会是最有效的)。
Yandex的所有服务都在它们页面的CSS和JavaScript代码以及XSL模板中用到了BEM。例如:
Yandex.Maps
Yandex.Images:提供在因特网上搜索图片的服务。
Yandex.Video:同时提供托管和搜索图片的服务。
Yandex.Auto
Turkish Yandex
一此服务并没有使用XSL模板而是使用我们最新的模板技术来构建他们的页面。就是我们前面提到的BEMHTML模板引擎。以下就是这些服务:
Yandex Search或者Yandex Search in English
Mobile Apps Search:这个网站是呈现在智能手机上的。
也有一些其他公司在使用BEM。
比如,mail.us就在他们的部分服务中用到了BEM。他们页面上的一些块的CSS代码就是基于BEM的。他们也有自己的C++模板引擎,并根据BEM方法论来编写块模板。
更多的案例:
Rambler.News
HeadHunter
TNK Racing Team
你或许也对那些使用bem-bl块库的网站感兴趣:
基于BEM的web表单JZ验证
Mikhail Troshev vCard:源代码托管在GitHub。
webpack css-loader 解决之道
BEM主要被诟病的一点在于其命名过长,结合Angular这种带有标签指令的框架时,整个HTML看起来会更混乱:
<!-- 发帖页面 --><span ng-repeat="post in postData track by post.id" ng-if="$index === 0" class="page-post__post-item" ng-class="{'page-post__post-item--even': $even}" popover-content=""></span>
当然,我们可以通过换行来缓解这个问题:
<!-- 发帖页面 --><span ng-repeat="post in postData track by post.id"
ng-if="$index === 0"
class="page-post__post-item"
ng-class="{'page-post__post-item--even': $even}"
popover-content=""></span>
但其实说穿了,BEM保证样式不冲突的核心就是:在元素名中加入唯一的标识。这个标识在BEM中对应的是模块名,也可能是一个独一无二的乱序字符串。
为模块中每个元素名加入标识,这可是重复的工作啊,重复的工作就应该交给机器去做。
webpack加载器css-loader,可在js中读取css样式,自2015年4月份起,该插件加入了placeholder功能,使得该插件可以解决CSS作用域的问题,原理也就是给元素的名称加入唯一的标识。
/* 分页组件 */:local(.prev) {}
css-loader加载器自定义的语法::local(.identifier){}向外暴露出选择器.prev。在JS代码中,我们可以拿到这个选择器:
import styles from './page-btn.css';var $prevBtn = $('<button class="' + styles.prev + '">上一页</button>');// ...
styles.prev返回的是一串独一无二且随机的字符串,该字符串对应着样式文件中的选择器。这名字有悖语义化,但css-loader支持配置字符串的生成格式,有兴趣的童鞋可以看看这篇文章:The End of Global CSS。
如对本文有疑问,请提交到交流论坛,广大热心网友会为你解答!! 点击进入论坛