BEM进化史

特别声明:此篇文章由David根据的英文文章原名《The Evolution Of The BEM Methodology》进行翻译,整个译文带有我们自己的理解与思想,如果译得不好或不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:http://coding.smashingmagazine.com/2013/02/21/the-history-of-the-bem-methodology以及作者相关信息

——作者:

——译者:David

这篇文章是研究BEM进化的一个案例,BEM是一种方法论,能让团队成员通过使用一种由统一语言构成的简单而强大的术语:块(blocks)、元素(elements)、修饰符(modifiers)来进行合作和思想沟通。一个大公司总会面临这样的挑战,即在逐步建立服务生态系统的同时也伴随着开发团队的不断增长。

很久以前,在一个遥远的国度,有一个叫Yandex的IT公司开始开发互联网搜索和一些相关的服务。时光荏苒,随着他们的服务不断增长,有越来越多的前端开发者不知疲倦地在为改善Yandex生态系统付出努力。他们做着伟大的事业,创造着神奇的工具,好让他们的开发人员干起活来得更加得心应手。现在是把这些知识分享给社区的时候了。借着开源的魔力来造福所有人。

众所周知,前端开发者有着永无止境地好奇心,喜欢创新,超级懒惰。这驱使他们为了节省宝贵的时间,为了让一切变得统一和自动化,不惜设计一个复杂的系统。

让我们回到2005年,趴在一个忙得焦头烂额的Yandex前端的肩膀后面,偷偷瞄上一眼,我们看到……

……一切从这儿开始

回到了2005年,焦点仍然是服务器端的一大堆事情。越过那个前端开发者的肩膀,我们看到一个典型的Yandex项目就是一些静态HTML页面的集合,将其作为构建高级模板的基础引用,就和XSL样式表一样。这些页面检出(checkout)后放在一个单独的目录里,像下面这样:

about.html
index.html
…
project.css
project.js
i/
  yandex.png	

每个页面都有一个与之对应的静态HTML文件,所有的CSS都合并在唯一一个样式表里,即project.css。所有的JavaScript也合并在唯一一个project.js文件里。并且,所有的项目页面共享这两个文件。2005年的时候JavaScript应用得还很少,所以,所有互动效果的代码可以保持很小的体积合并在一个文件里。图片文件则存放在一个单独的目录里,因为它们数量很多。在IE5的那个年代加之当时还没有CSS3,图片被用作实现各种各样的效果,甚至是圆角(比你老的Web开发人员可能会相信我)。

为了保持基本结构,会用简单的CSS注释来区分一个页面中不同章节的样式:

/* Content container (begin) */
#body{
  font: 0.8em Arial, sans-serif;
  margin: 0.5em 1.95% 0.5em 2%;
}
/* Content container (end) */
/* Graphical banner (begin) */
.banner{
  text-align: center;
}
.banner a{
  text-decoration: none;
}
/* Graphical banner (end) */	

这些ID和class会出现在HTML标记中。

一些HTML会被手动粘贴到生产环境的XSL样式表里,而且所有的改变都会被手动双向同步。这么做相当麻烦,如果嫌麻烦不这么做的话,那么更新就会不及时。

中等规模的项目

从2006年开始,第一版的Yandex.Music陷入了沉重地发展。很多页面没有很好地融入人们熟悉的极简概念。许多CSS类不得不起一个有意义的名字,太多毫无意义的依赖在整个项目中泛滥开来。所有这一切都在呼唤一个更好的解决方案。/p>

下面就是当时一段典型的CSS代码:

/* Albums (begin) */
.result .albums .info{
  padding-right: 8.5em;
}

.result .albums .title{
  float: left;
  padding-bottom: 0.3em;
}

.result .albums .album .listen{
  float: left;
  padding: 0.3em 1em 0 1em;
}
.result .albums .album .buy{
  float: left;
  padding: 0.4em 1em 0 1.6em;
}
.result .albums .info i{
  font-size: 85%;
}
/* Albums (end) */	

代码中到处都是长长的层叠规则。

来看另一个:

/* Background images (begin) */
.b-foot div{
  height: 71px;
  background: transparent url(../i/foot-1.png) 4% 50% no-repeat;
}
.b-foot div div{
  background-position: 21%;
  background-image: url(../i/foot-2.png);
}
.b-foot div div div{
  background-position: 38%;
  background-image: url(../i/foot-3.png);
}
.b-foot div div div div{
  background-position: 54%;
  background-image: url(../i/foot-4.png);
}
.b-foot div div div div div{
  background-position: 71%;
  background-image: url(../i/foot-5.png);
}
.b-foot div div div div div div{
  background-position: 87%;
  background-image: url(../i/foot-6.png);
}
/* Background images (end) */	

注意,以上许多规则都是以ID和标签名作为选择器。

与此同时,一个更大的项目启动了wow.ya.ru一个博客平台,一个供人们互动,分享,阅读,参与的地方。

有一大堆各式各样的页面要支持。如果用之前的旧办法,代码将在很多层面上失去控制。

用块来拯救

我们需要指定一个数据域来管理页面接口对象。这是一个方法论的问题:我们需要明确我们在使用诸如类(class)、标签、可视化组件等等的这些概念来工作时的方式。

对于一个Yandex项目中的典型Web页面,HTML结构和CSS样式依然是我们开发工作的重点,JavaScript只作为辅助技术。为了能够更容易地维护一个组件的HTML和CSS,一个新的术语被发明了:“块(block)”。一个块是设计或布局的一部分,它有具体且唯一地意义 ,要么是语义上的要么是视觉上的。

在大多数情况下,任何独立的页面元素(或复杂或简单)都可以被视作一个块。它的HTML容器会有一个唯一的CSS类名,也就是这个块的名字。

针对块的CSS类名会加一些前缀(b-,c-,g-),这些前缀在CSS中有类似命名空间的作用。虽然命名约定在后来有所修改,但下面所列是带有注解的最初版本:

b- 块(block)
一个独立的块,只要你需要你可以把它放在页面的任何地方。
с- 控件(control)
一个控件(也是一个独立的块),与JavaScript对象绑定。
g- 全局(global)
一个全局定义,很少使用而且总是为某些特殊的目的才定义的。这样的定义越少越好。

有些后缀也会被用到,比如:

-nojs 禁用JavaScript(no JavaScript)
一个在JavaScript不可用的情况下出现的样式规则。在onload回调方法里可以从所有Dom节点上移除这些后缀,将它们标记为在JavaScript可用时的样式。

块里面是什么?

在一个HTML块容器的内部,一些节点有不同的CSS类。这不仅仅促进了创建与标签名称分离的样式规则,同时每个节点也扮演着更有意义的角色。这些节点是“block elements”或者是简单的“elements”。

块(block)和元素(element)最主要的区别就是,一个元素不能存在于其父级块容器的外面。如果一个东西不能从一个块里分离出来,那么它就是元素。能分离出来的元素本身就应该(可能)是块。

起初,元素只能存在于块容器内,后来出现了一种技术,既能让元素存在于块外,又能保证块的完整性。

在样式表里,与元素相关的大部分CSS会有额外缩进并用注释包裹:

/* Head (begin) */
.b-head { … }
  
  /* Logo (begin) */
  .b-head .logo { … }
  .b-head .logo a { … }
  /* Logo (end) */

  /* Right side (begin) */
  .b-head .right { … }

    /* Info (begin) */
    .b-head .info { … }
    .b-head .info .exit a { … }
    /* Info (end) */

    /* Search (begin) */
    .b-head .search { … }
    .b-head .search div div, .b-head .search div div i { … }
    /* Search (end) */
  /* Right side (end) */
/* Head (end) */	

项目文件结构演化

在yandex,一个前端开发人员会同时参与不只一个项目。当所有的项目使用相同(或相近)的文件结构时,开发人员在不同的资源库和各种分支之间切换会比较容易。粒度(granularity)是另一个关注点,因为它不仅可以为版本控制系统提供更多的灵活性,还有益于并行开发过程中避免冲突。p>

这引出一个更加统一的结构:CSS、JavaScript和图片文件要放在单独的目录里。在CSS中有专门的文件是针对IE的具体解决方案,以保证核心代码是干净且符合标准的。在生产环境下,IE会通过只有它自己可以识别的条件注释来获取其相应的CSS。

随着对JavaScript的使用越来越多,也附加了一些可供选择的组件和库。

index.html
css/
  yaru.css
  yaru-ie.css
js/
  yaru.js
i/
  yandex.png	

对于IE特定的hack,如果它们符合CSS标准,就把它们合并到CSS主文件里(yaru.css):

/* Common definitions (begin) */
body{
  font-family: Arial, sans-serif;
  font-size: 0.8em;
  padding: 0 0 2em 0;
  background: #fff;
}

* html body{
  font-size: 80%;
}	

一个效率不高的方案是把它们放在一个单独的yaru-ie.css的文件里(在IE的条件注释中加载):

/* Common blocks (begin) */
/* Artist (begin) */
.b-artist .i i {
  top: expression(7 + (90 - this.parentNode.getElementsByTagName('img')[0].height)/2);
  filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../i/sticker-lt.png', sizingMethod='crop');
}	

开始构建一个框架

设计类似的项目最终意味着一遍又一遍地重建相同的块(block)。Yandex作为一个提供上百种服务的门户网站,同时这些服务又共享相同的样式,因此,在这种规模下容不得半点马虎,光凭复制和粘贴是行不通的。所以我们就从那些我们内部称为通用块的片段开始,通过一些小小的修改让它们成为可重用的组件。第一批被统一的页面片段是页眉(header)、页脚(footer)和一些CSS排版元素。与之对应的文件托管在内部的专用服务器上(common.cloudkill.yandex.ru见下面的清单)。这就是我们早期的统一框架。

样式会从服务器直接导入(import):

@import url(http://common.cloudkill.yandex.ru/css/global.css);
@import url(http://common.cloudkill.yandex.ru/css/head/common.css);
@import url(http://common.cloudkill.yandex.ru/css/static-text.css);
@import url(http://common.cloudkill.yandex.ru/css/foot/common-absolute.css);
@import url(http://common.cloudkill.yandex.ru/css/foot/common-absolute-4-columns.css);
@import url(http://common.cloudkill.yandex.ru/css/list/hlist.css);
@import url(http://common.cloudkill.yandex.ru/css/list/hlist-middot.css);
@import url(http://common.cloudkill.yandex.ru/css/dropdown/dropdown.css);
@import url(http://common.cloudkill.yandex.ru/css/dropdown/dropdown-arrow.css);
@import url(slider.css);

/* Header (begin) */
  /* Service (begin) */
    .b-head .service h1 { … }
    .b-head .service h1, .b-head .service h1 a, .b-head .service h1 b { … }	

很显明,这里用了太多的导入(import)。所以我们决定在部署前对样式进行预编译(后面我们也会对JavaScript文件做相同的处理)。文件的实际内容将替换@import命令(这个过程叫做“内联(inlining)”),并且会进行优化。我们内部使用的内联工具是从一个开源项目Borschik中提取的一段脚本打包而来的。建议你也试一试!

块的概念

2007年的秋天,我们从平常的实践中获得一些理论。这是我们在理解了HTML和CSS布局之后的又一基本思想,即一个独立的块的概念,它在2007年于俄罗斯莫斯科举办的ClientSide大会上被提出。在这次演讲中,我们第一次尝试给一个块下定义。

块的独立宣言

一个块的正式(实际上是半正式的)定义有下面三个基本原则:

  1. CSS中只能使用类名(不能是ID)。
  2. 每一个块名应该有一个命名空间(前缀)
  3. 每一条CSS规则必须属于一个块。

只要把那些唯一的ID删除了,一个块就可以在同一个页面上使用多次。这也允许在同一个Dom节点上可以有两个及以上的类共存,后来证明这么做非常有用。

简单块和复合块的误分类

我们定义一个“简单”块是它的内部任何地方都不能有其他块。“复合”块则允许(甚至是要求)有嵌套的块。

这种分类简直太天真了。即使是最简单的块都有可能被其他块包裹,以至于不得不“升级”、重构以适应新的角色。这种误分类常常导致事与愿违,事实上,我们最终接受了完全相反的原则:只要有可能,任何块都应该允许嵌入任意内容。

完全独立的块

CSS定义并不是无懈可击的,比如当我们从不同的源混合大量的样式内容到某个页面的时候。在复杂布局中,块会因为元素名称的冲突而改变彼此的外观。基于标签名的CSS规则可能匹配更多它本不想匹配的节点。因此,一个严格版本的独立块(叫做“完全独立的块”或者CIB(completely independent block))的定义产生了,它有以下的补充规则:

1.永远不用标签名来匹配CSS,一切都用类名。

.b-user b → .b-user .first-letter	

2.一个块中元素的类名必须用父级块的名称作为前缀。

.b-user .first-letter → .b-user-first_letter	

这么做导致类名很长,而且结果是HTML文件变得相当大。

这就是我们为什么认为CIB是个代价高昂的解决方案的主要原因,它更多地被用作补救办法而不是平常实践。

前缀

你肯定明白,命名变量永远是开发中最难的问题之一。因此我们谨慎对待,提出了四个可用于块名称的前缀,每一个都有它自己的语义。

b-
普通块
h-
用于把一些元素粘合在一起
l-
布局网格(layout grids)
g-
全局样式

修饰符

一个“修饰符”可以理解为一个块的特定状态,标识着它持有一个特定的属性。

用一个例子来解释最好不过了。一个表示按钮的块默认有三个大小:小,中,大。为了避免创建三个不同的块,最好是在块上加修饰符。这个修饰符应该有个名字(比如:size)和值(small,normal或者big)。

有两个原因会导致一个块改变它的呈现状态:

  1. 一个块的状态可能因为它在布局中的位置发生改变而改变。这叫做“依赖上下文”的修改。
  2. 一个附加(后置)的类名可以改变一个块的外观,让其被其他CSS样式作用。这叫做“上下文无关”的修饰符。
class="b-block b-block-postfix"	

一个统一的全网框架

2008年初,Yandex正着手检讨自己的内部设计策略。我们决定写一本品牌手册(内部使用)一本让全公司在实施界面设计时的最佳实践。

接受这一任务的是前端团队,经过各种思考之后,我们决定继续使用熟悉的技术:HTML和CSS。

Web界面发展很快,快到任何一种文字或图片在企图描述它的时候就已经晚了。我们需要一本快速更新的品牌手册来统一各种不同的Yandex服务和产品之间的差异。

因此我们决定,用于建立品牌手册界面的块和建立我们网站的是同样的块。这些块不但可以在项目之间共享,而且可以代表Yandex最新的界面设计思想。

我们决心开发一个全网的框架,让所有人从中受益同时也可以收到他们的回馈。我们内部给这个项目起名叫“Lego”。

首要问题是框架库的结构

不同的实现有相同的顶层目录结构:

css/
html/
js/
xml/
xsl/

每一种实现也有各自的子目录结构。

CSS目录下有三个文件夹:

css/
  block/
    b-dropdown/
      b-dropdown.css
  service/
    auto/
      block/
        b-head-logo-auto.css
      head.css
  util/
    b-hmenu/
      b-hmenu.css	
block
在服务间共享的块。
util
开源的通用块。
service
用于专门的Yandex服务的CSS样式,供品牌,页眉,页脚等等使用的。

HTML目录结构和CSS是一样的:

html/
  block/
    b-dropdown.html
  service/
    auto/
      l-head.html
  util/
    b-hmenu.html	

然而,JavaScript的目录结构松散,各服务之间使用也不一致:

js/
  check-is-frame.js
  check-session.js
  clean-on-focus.js
  dropdown.js
  event.add.js
  event.del.js	

每一个服务都有一个相同的描述其页面头部的XML文件(提供了具体项目所必需的数据)。结合一个XSL样式表,这个XML文件足可以生成一个HTML页眉的代码。

xml/
  block/
    b-head-tabs-communication.xml
    common-services.ru.xml
    head-messages.ru.xml
  service/
    auto/
      head.xml	

各式块(一个块一个文件)的XSL模板放在一个目录内:

xsl/
  block/
    b-dropdown.xsl
    b-head-line.xsl
    i-common.xsl
    i-locale.xsl
    l-foot.xsl
    l-head.xsl	

如何整合?

Lego和各项目之间依靠svn:externals来进行版本控制。

当建立一个用于部署生产环境的包时,代码会从外部库(Lego)嵌入这个包中,类似将静态库链入编译语言。

Lego为其每一次重要发布都提供了一个SVN分支。我们允许项目对svn:externals中的某个分支打一些补丁,为了产品稳定起见,一个项目可能只会使用某个特定Lego修订版。任何情况下,主版本都应该预留以做不时之需。

这种简单的技术保证了足够的灵活性,并且直到今天Yandex的很多服务中都还在使用。

每个页面文件

会用到的块的CSS文件会用import从Lego目录结构中导入当前页面。

@import url(../../block/l-head/l-head.css);
@import url(../../block/b-head-logo/b-head-logo.css);
@import url(../../block/b-head-logo/b-head-logo_name.css);
@import url(block/b-head-logo-auto.css);	

这些导入命令是手动来维护的。

当时,我们尝试了几种方法但是还是没有形成一个统一的文件名约定。

全网框架之Lego 1.2(2008)

在Lego 1.2发布的时候,我们了重构了代码,而且目录结构也发生了变化。

common/
  css/
  js/
  xml/
  xsl/
example/
  html/
service/
  auto/
    css/
    xml/	

先前通用块的样式是分置在util目录里的各个块目录中。现在,这些共享的通用样式被移动到common/css。我们一直想开放这些源代码,但真正这么做是在两年后了。

common/
  css/
    b-dropdown/
      arr/
        b-dropdown.arr.css
        b-dropdown.arr.ie.css
        b-dropdown.css
        b-dropdown.ie.css	

IE专用样式从*-ie.css改名为*.ie.css。

所有可供选择的CSS文件(比如b-dropdown_arr.css)移动到了单独的目录里(比如arr/b-dropdown.arr.css)。

对于一个块的修饰符类名,替换了之前名称中的单横线,用下划线来做分隔符。这样可以让一个块的名称从视觉上与一个修饰符的名称区分开。当我们用自动化开发工具做模糊查询和模式匹配的时候它显得非常有用。

2009年的BEM

2009年3月,Lego 2.0发布。这标志着BEM方法论的兴起。

BEM代表“块(block),元素(element),修饰符(modifier)”,我们常用这三个实体开发组件。

BEM

2009年的Lego 2.0

是什么推动了2.0版本的升级?

它建立在以块的概念作为主导思想的底层实现技术上。

每个块包含在一个单独的目录里,每一种技术(CSS,JavaScript,XSL等)都有一个单独的文件。开发文档有它自己的文件类型,比如.wiki。

我们当时还要遵循其他什么原则么?

术语摘录

一个“独立块”可能被放在任何Web页面的任何地方。因为我们使用的是XML和XSL的模板,所以一个lego命名空间下的节点就代表一个块。

XML:

<lego:l-head>
<lego:b-head-logo>	

在HTML中一个块容器节点的class属性值与这个块的名字完全对应。

HTML

<table class="l-head">
<div class="b-head-logo">	

CSS

.l-head
.b-head-logo	

所有与块相关的文件(CSS,JavaScript,HTML,XSL)都放在对应的块目录里。

common/
  block/
    b-head-logo/
    b-head-logo.css
    b-head-logo.xsl
    b-head-logo.js
    b-head-logo.wiki	

XML文件定义了页面的结构,块用带有lego命名空间的节点定义(这里省略了块名称前缀)。

<lego:b-head-logo>
</lego:b-head-logo>	

块中元素的HTML代码class属性值的前缀也省略了。

<div class="b-head-logo">
  <span class="name">Web Service Name Here</span>
</div>
.b-head-logo .name { … }	

每个块元素的描述文件都有自己的目录。

common/
  block/
    b-head-logo/
      name/
        b-head-logo.name.css
        b-head-logo.name.png
        b-head-logo.name.wiki	

在XML文件中,用lego命名空间的节点属性来指定修饰符(译注:这里是lego:theme属性)。

<lego:b-head-tabs lego:theme="grey">	

在HTML中就会加上一个额外的类名

<div class="b-head-tabs b-head-tabs_grey">

.b-head-tabs_grey { … }	

与某个修饰符相关的文件放一个单独的目录中,用下划线作为修饰符前缀:

common/
  block/
    b-head-logo/
      _theme/
        b-head-logo_gray.css
        b-head-logo_gray.png
        b-head-logo_gray.wiki	

XML中的声明

项目中用到的所有Lego组件都在一个XML文件中声明:

<lego:page>
  <lego:l-head>
    <lego:b-head-logo>
      <lego:name/>
    </lego:b-head-logo>

    <lego:b-head-tabs type="search-and-content"/>	

XML允许导入CSS:

@import url(../../common/block/global/_type/global_reset.css);
@import url(../../common/block/l-head/l-head.css);
@import url(../../common/block/b-head-logo/b-head-logo.css);
@import url(../../common/block/b-head-logo/name/b-head-logo.name.css);
@import url(../../common/block/b-head-tabs/b-head-tabs.css);
@import url(../../common/block/b-dropdown/b-dropdown.css);
@import url(../../common/block/b-dropdown/text/b-dropdown.text.css);
@import url(../../common/block/b-pseudo-link/b-pseudo-link.css);
@import url(../../common/block/b-dropdown/arrow/b-dropdown.arrow.css);
@import url(../../common/block/b-head-search/b-head-search.css);
@import url(../../common/block/b-head-search/arrow/b-head-search.arrow.css);
@import url(../../common/block/b-search/b-search.css);
@import url(../../common/block/b-search/input/b-search.input.css);
@import url(../../common/block/b-search/sample/b-search.sample.css);
@import url(../../common/block/b-search/precise/b-search.precise.css);
@import url(../../common/block/b-search/button/b-search.button.css);
@import url(../../common/block/b-head-userinfo/b-head-userinfo.css);
@import url(../../common/block/b-head-userinfo/user/b-head-userinfo.user.css);
@import url(../../common/block/b-user/b-user.css);
@import url(../../common/block/b-head-userinfo/service/b-head-userinfo.service.css);
@import url(../../common/block/b-head-userinfo/setup/b-head-userinfo.setup.css);
@import url(../../common/block/b-head-userinfo/region/b-head-userinfo.region.css);
@import url(block/b-head-logo/b-head-logo.css);
@import url(block/b-head-search/b-head-search.css);	

在这个例子中,先导入通用样式,然后再导入项目的样式。这么做我们就可以修改基于通用共享库的具体项目的代码。

同样可以用下面的方法导入JavaScript:

include("../../common/block/i-locale/i-locale.js");
include("../../common/block/b-dropdown/b-dropdown.js");
include("../../common/block/b-search/sample/b-search.sample.js");
include("../../common/block/b-head-userinfo/user/b-head-userinfo.user.js");	

同样可以用基于XML的声明来导入XSL模板:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">

<xsl:import href="../../common/block/i-common/i-common.xsl"/>
<xsl:import href="../../common/block/i-items/i-items.xsl"/>
<xsl:import href="../../common/block/l-head/l-head.xsl"/>
<xsl:import href="../../common/block/b-head-logo/b-head-logo.xsl"/>
<xsl:import href="../../common/block/b-head-logo/name/b-head-logo.name.xsl"/>
<xsl:import href="../../common/block/b-head-tabs/b-head-tabs.xsl"/>
<xsl:import href="../../common/block/b-dropdown/b-dropdown.xsl"/>
<xsl:import href="../../common/block/b-pseudo-link/b-pseudo-link.xsl"/>
<xsl:import href="../../common/block/b-head-search/b-head-search.xsl"/>
<xsl:import href="../../common/block/b-search/b-search.xsl"/>
<xsl:import href="../../common/block/b-search/input/b-search.input.xsl"/>
<xsl:import href="../../common/block/b-search/sample/b-search.sample.xsl"/>
<xsl:import href="../../common/block/b-search/precise/b-search.precise.xsl"/>
<xsl:import href="../../common/block/b-search/button/b-search.button.xsl"/>
<xsl:import href="../../common/block/b-head-userinfo/b-head-userinfo.xsl"/>
<xsl:import href="../../common/block/b-user/b-user.xsl"/>
<xsl:import href="../../common/block/b-head-userinfo/service/b-head-userinfo.service.xsl"/>
<xsl:import href="../../common/block/b-head-userinfo/setup/b-head-userinfo.setup.xsl"/>
<xsl:import href="../../common/block/b-head-userinfo/region/b-head-userinfo.region.xsl"/>

</xsl:stylesheet>	

代码生成是重要的一步,从此刻开始,我们不再依赖手动维护。

回顾CSS选择器的速度(2009)

2009年重新改版的Yandex.Mail服务,把界面响应的整体速度放在首位。我们当初想要开发出一个像桌面软件一样快的Web应用,有可能比桌面软件更快。

客户端(即浏览器)XSL转换作为主要的模板解决方案(XML的数据单独加载)。根据当初的测试,XSL转换的速度是即时的,基本是瞬间完成,但是要把转换后的HTML代码追加(append)到Dom中却很耗时。然而,禁用掉CSS后问题莫名消失了。

经过详细研究影响渲染速度的各种因素,我们发现CSS选择器是罪魁祸首。越大的DOM树和CSS样式表,就会花越多的时间来渲染。

使用尽可能简单的CSS选择器且尽可能减少CSS层叠,这样CSS样式渲染就会更快。单个类名的选择器会快很多,而且浏览器处理起来也更容易。我们已经有了一个解决方案,可以使用所谓的“完全独立的块”(CIB) 这样的选择器。

所有的Lego块都遵循CIB规则。所有的类名都改为独一无二的,这样一来,绝大多数的CSS样式的选择器只有一个类名,运行起来更快。

<div class="b-head-logo">
  <span class="b-head-logo__name">
    Web Service Name Here
  </span>
</div>	

建立命名约定

经过多次尝试修改命名约定之后,我们一致同意不对原则性的东西做改变。

用双下划线(__)替换文件名中的点分隔符:

前:b-block.elem.css
后:b-block__elem.css	

这样的话,文件名和CSS选择器将保持一致。

块元素也允许有它们自己的修饰符。因此,.b-block__elem_theme_green>

 

前: .b-menu__item_current
后: .b-menu__item_state_current	

这么修改后,JavaScript操作修饰符变得更容易。

走向开源(2010)

2010年,我们在GitHub上传了部分代码,让它成长为一个开源项目。

创建BEM-BL库

Lego中的块正逐步移植到bem-bl,这个块的库不仅仅是适用于Yandex项目,也适用任何网站。随着这些块的开源,我们也在逐步地改善代码和添加特性。

这是个不断进行地工作,我们邀请每个人对它提出要求。同时,我们也开发了一些bem工具,就是一个辅助脚本集和一些自动化实用工具,来配合BEM文件工作让它使用起来更方便。这些基本上是用Node.js完成的,目的是让那些熟悉JavaScript和甘愿奉献的前端开发人员没有那么高的门槛。

BEM重定级

任何东西都不是放之四海皆准的,但BEM是个例外。由于块和元素也代表着文件系统的中的文件和目录,BEM的文件结构是统一的,主要基于语义标准,我们很容易重定义一个块并为其添加功能。像JavaScript中对对象的扩展一样,我们也可以对BEM的块进行扩展,我们称之为“重定级(redefinition levels)”。

一个典型的重定级主要有下面几种情况:

  1. 在GitHub检出的一个公共的BEM-BL库上扩展……
  2. 在一个内部的库(如Lego)上扩展……
  3. 在某个具体的项目中的库上扩展……

你可以自由地添加更多的级别。或许你需要在一些具体的页面上升级你的块……你懂得!

比如:

bem-bl/
  b-logo/
lego/
  b-logo/
auto/
  blocks/
    b-logo/	

我们可以为个别的重定级使用自定义文件结构。只要你遵循BEM的理念,你需要做的只是根据你的新文件结构配置我们的构建工具。我们在这里不会涉及太多的细节,但是这里有个配置文件:

.bem/
  level.js	

你可以指定不同的文件命名模式,甚至可以让你的目录结构完全扁平化。

BEMHTML模板引擎

我们尝试过不同的模板解决方案,最终还是开发了我们自己的一套,叫做BEMHTML。

这款模板引擎:

  1. 基于核心的BEM规则(块,元素,修饰符)操作;
  2. 支持重定级
  3. 在浏览器和服务器端模板会被预编译成JavaScript代码运行。

更多的关于BEMHTML的信息可以访问这里:

尝试BEM!

正如你所看到的BEM经历了很长的发展,不断地测试,不断地修正。Yandex花费了一些时间来找出什么是重要的什么是不重要的。

BEM方法论的基础是块,元素,修饰符。我们在所有的项目中都使用了它们。

众所周知,BEM不是最终标准答案,也不是什么真理准则,只是从现实项目的不断实践和测试中得出的一个方法。你可以去其糟粕取其精华。

BEM是一种相当灵活的方法。没有所谓的BEM API或者BEM SDK。但是我们还是鼓励你尝试我们提供的BEM框架开源工具,你可以找到一种适合你自己产品或技术的BEM规则。

让我们简单地来看个例子。

一个文件中的多个块

假设你有个Web项目打算在HTML和CSS中使用BEM。太棒了,我们这就开始!

选一种你认为最容易理解和维护的命名方法。比如,你给你的块元素一个简单的类名(非前缀),然后添加上键值对类型的修饰符。

.b-block
.b-block .elem
.b-block_size_l
.b-block .elem_size_l

你可以更进一步给你这个有语义的块中的所有Dom节点上指定一个具体的类(就是我们上面提到的“完全独立的块”)

.b-block
.b-block__elem
.b-block_size_l
.b-block__elem_size_l	

觉得CSS前缀太长?那就删掉它们!

.block
.block__elem
.block_size_l
.block__elem_size_l	

这真是尝试BEM理念的绝佳机会。因为我们没有严格的规定,所以也不存在什么破坏规则的事情,只要你遵循块,元素,修饰符的主要原则就行。

给你使用的每一种技术建个单独的文件,把所有关于块的声明放在一起:

myfacebook/
  myfacebook.css
  myfacebook.js
  myfacebook.html

虽然在这一阶段所有修改都是你手动操作的(没用BEM工具),但是这会让你学得更快。

把块放进单独的目录

随着项目进展,你会发现把块放到单独的文件里会比较方便。再给这些块建个目录:

blocks/
  b-myblock.css
  b-myblock.js
  b-yourblock.css
  b-yourblock.js	

此时,你应该建立一个你的JavaScript和CSS文件来把你的块整合起来(也就是说,把所有分开的块样式整合到一个CSS文件中),你可以尝试一下BEM工具(bem-tools).

可选项

一些块中的元素和修饰符可能只会用在某些页面或是个别情况下,为了保证核心文件的尺寸和整洁,你可以分开加载这些可选的元素:

blocks/
  b-myblock/
    b-myblock_mod_val1.css
    b-myblock__opt-elem.css
    b-myblock__opt-elem_mod_val1.css
    b-myblock.css	

修饰符目录

对于有很多修饰符的块来说可以把这些修饰符放在一个单独的目录里:

blocks/
  b-myblock/
    _mod/
      b-myblock_mod_val1.css
      b-myblock__opt-elem.css
      b-myblock__opt-elem_mod_val1.css
    b-myblock.css	

这么做可以让这个块的根目录维护起来更方便。

可选元素目录

块的元素也应该是可选的,并且放置在不同的目录里。这是个非常灵活的高级用法。

blocks/
  b-myblock/
    _mod/
      b-myblock_mod_val1.css
    __opt-elem/
      b-myblock__opt-elem.css
    b-myblock.css	

这就是我们当初开发BEM-BL库和大部分Lego块的方法。

把所有东西都放进一个目录

你可以给每个元素和修饰符一个单独的目录,可选或不可选。这样在逻辑上非常清晰,但是你可能会发现这种一致的目录结构更难维护:

blocks/
  b-myblock/
    _mod/
       b-myblock_mod_val1.css
    __elem/
       b-myblock__elem.css
    b-myblock.css

你不用读一行代码就能从一个块的目录结构上知道这个块的构造。尽管它是有代价的,但是它毕竟是达到了前所未有的透明度。

我们尚未完全决定在Lego中采用这种方法,但我们最有可能选择的就是这种方式。

总结

根本没有绝对正确的BEM,我们也并不打算创造一个。我们所提供的这种实现只是我们更喜欢的那种,你也可以有你自己的BEM,只要你遵循BEM的核心原则。

BEM是一个方法论。公司和团队可以在把BEM整合到现有的工作流中的同时找出最适合自己的。

鸣谢

这篇文章是基于Vitaly Harisov在明斯克(白俄罗斯首都)的Yandex周六会议上的一个介绍性演讲而来的。Vitaly Harisov是BEM方法论的创立者之一。

译者手语:整个翻译依照原文线路进行,并在翻译过程略加了个人对技术的理解。如果翻译有不对之处,还烦请同行朋友指点。谢谢!

关于David

2009年开始接触前端开发,2011年组建创业团队——[五维互动],2012年团队被“收编”并更名[创影互动],遂只身来上海发展,现在就职于FlipScript。欢迎交流共勉:腾讯微博个人博客

如需转载烦请注明出处:

英文原文:http://coding.smashingmagazine.com/2013/02/21/the-history-of-the-bem-methodology/

中文译文:http://www.w3cplus.com/css/the-history-of-the-bem-methodology.html

返回顶部