一.BEM简介
BEM自称是前端开发方法论(Frontend Development Methodology),提供了包括命名规则、CSS/JS模块化原则、构建工具在内的一套用于开发环节的方法
P.S.强调开发环节是为了与Yeoman之类的东西区分开,BEM想说的是方法,而不是工具
提供了一套命名规则(BEM命名规则),主要用于模块化CSS,但BEM中也用在了JS以及文件命名等各个方面
CSS中主要解决了这些问题:
团队协作中样式命名(比如class)冲突
用长
class
名解决,不使用结构化选择器(如子选择器、后代选择器等等),类名自带层级关系实现代码自己会说话(self-documenting code)的目标
语义化类名,提供更多的信息,例如元素名、功能、所属组件名等等
避免组件之间相互影响
不依赖结构化选择器,全靠类名
怎样才能在同一DOM节点上组装各种特性,同时避免重复代码(复制粘贴)
Mix,例如
div.classA>div.classB
组合Block A和Block B
这些解决方案在如何写好CSS都有说明,如果只是定义了一套长类名格式的话,确实没什么意义,所以BEM还为这一套命名实现了框架和工具,确保开发过程中能用好用
二.术语概念
(建议跳过,直接看红色部分,官方解释等于没解释)
Block
逻辑和页面功能都独立的页面组件,是一个可复用单元,特点如下:
可以随意嵌套组合
可以放在任意页面的任意位置,不影响功能和外观
可复用,界面可以有任意多个相同Block的实例
Element
Block的组成部分,依赖Block存在(出了Block就不能用)
Modifier
[可选]定义Block和Element的外观及行为,就像HTML属性一样,能让同一种Block看起来不一样
BEM entity
上面三个都是
Mix
单一DOM节点上各个BEM entity构成的一个实例,特点如下:
能把几个BEM entity的行为、样式结合起来,避免代码重复
基于现有BEM entity语义化地创建新的界面组件
BEM tree
用BEM对web页面结构进行描述
Block implementation
很多不同的技术决定了BEM entity的行为、外观、测试、模板、文档、依赖描述、额外数据(例如图片)等方面
Implementation technology
用来实现一个Block的技术,可以是一种或者多种
Block redefinition
在不同层面上通过给Block添加新特性来修改Block的实现
Redefinition level
一系列BEM entity及其部分实现
使用中可以看作逻辑层级,例如project level、library level,前者可以重写后者Block的功能及外观
P.S.实际是通过文件目录和按顺序引入实现的层级隔离和重写
需要关注的点:
B(Block):表示模块,最小的可复用单元,功能独立,可以嵌套、组合使用
E(Element):B的组成部分
M(Modifier):表示E的状态(不同状态下的E有不同的功能和外观),也是B的组成部分
BEM tree:用BEM术语描述页面/项目结构
Block implementation:实现Block需要的东西,包括所有相关内容,比如JS,CSS,image等等
Redefinition level:可以看作逻辑层级,在高层可以重写/扩展低层Block(模块)
三.BEM命名规则
block-name__element-name_mod-name
,例如nav-menu__item_active
block-name
本身可能并不对应样式,而只作为Block的逻辑名
BEM只是提供一般方法,并没有限定必须使用这种命名规则
四.JavaScript for BEM
命名规则同样适用于JS
JS中,Modifier用来表达Block或者Element的逻辑(CSS中,Modifier用来定义外观),JS通过一系列状态来描述Block和Element的行为
不通过class
来查找组件,而是通过Block名(HTML中,不只通过class
唯一标识,也可以是标签、属性等等),例如:
document.querySelector('.button')
.addEventListener('click', function() {
document.querySelector('.popup').classList.toggle('popup_visible');
}, false);
// BEM Style(伪代码)
block('button').click(function() {
block('popup').toggleMod('visible');
});
实际应用需要引入i-bem.js
Modifier可以设置Block的具体状态,通过添加和移除Modifier来实现Block的状态切换,对Modifier的操作会触发事件,通知相关Block。为了确保这套机制正确运行,不应该允许时随意切换状态或者直接修改某个DOM节点的class,这些操作必须通过BEM提供的helper来实现(这种限制类似于svn本地仓库)
Modifier对应状态,状态关联功能。添加Modifier时,状态发生变化,自动应用相应功能;移除Modifier时,相应功能会被自动移除
定义Modifier和状态以及底层之间的关联:
block('button').onSetMod({
focused: {
true: this.onFocus,
'': this.onBlur
}
});
屏蔽了状态之下的东西(可能是添加/移除Modifier引起状态改变,或者是用户行为触发的),在编程层面需要关注的最小单元是状态(Modifier)。可以通过给Modifier添加样式来定义各个状态的外观,也可以通过Redefinition level来改变或者完全重写Block的行为
把代码分离到各文件中,遵循文件目录结构规则
规则:
每个Block的逻辑和可选的Element以及Modifier都放在单独文件里
每个组件的JS文件目录结构遵循BEM文件目录结构规则
示例:
logo/
logo.css # Block’s appearance
logo.tmpl # Templates for generating the block’s HTML representation
logo.js # Dynamic behavior of the block in the browser
便于管理、复用、移植、支持Redefinition level和组合使用
把逻辑分离到各个Redefinition level
在不同的Redefinition level实现新Block可以继承并扩展现有的Block,或者完全重写该Block,例如:
// 完全重写
block('button').onSetMod({
focused: {
true: this.someCustomOnFocused
}
});
// 部分重写
block('button').onSetMod({
focused: {
true: function() {
this.__base.apply(this, arguments);
this.someCustomOnFocused();
}
}
});
原则
JS中的Block与DOM节点没有强对应关系,比如有的Block没有DOM表现,只是单纯的逻辑块
Block之间的交互通过事件订阅来实现,比如:
订阅其它Block实例的事件
订阅Modifier变更
直接调用其它Block的公开接口
其它任何交互模式,比如Event Channel(消息机制)
Element只能通过所属的Block提供的API来访问,不能直接访问某个Block中的Element
五.文件目录结构
代码被分解成独立的小部分,便于开发独立Block,发布前组装起来进行优化
特点
把Block implementation分解到各个单独的文件中
便于管理(开发过程中)和移植(植入其它项目)
可选的Element和Modifier都存放在各个单独的文件中
只引入相关的Block implementation(按需加载)
文件按语义分组,而不是文件类型
比如所有Block都放在
block
目录下,以保证只引入需要的Block项目被划分成各Redefinition level
为了消除重复代码,便于重写/扩展现有Library Block,当Library Block更新时,不会影响上层的重写/扩展
目录结构示例
blocks/
input/ # input block directory
_type/ # type modifier directory
input_type_search.css # Implementation of modifier type
# with value search in CSS technology
__box/ # box element directory
input__box.css
input.css
input.js
button/ # button block directory
button.css
button.js
button.png
popup/ # popup block directory
_target/
popup_target.css # Common code of modifier target
popup_target_anchor.css # Modifier target with value anchor
popup_target_position.css # Modifier target with value position
_visible/
popup_visible.css # Boolean modifier visible
popup.css
popup.js
文件名也遵循BEM命名规则,Modifier之间的共享部分可以提出来,作为单独文件(popup_target.css
)
Redefinition level示例
library.blocks/
button/
button.css # CSS implementation in the linked library (height 20px)
project.blocks/
button/
button.css # Redefinition of CSS implementation (height 24px)
文件存放在不同的层级上,分离开避免互相影响
或者按平台分层:
common.blocks/
button/
button.css # Generic CSS implementation of the button
desktop.blocks/
button/
button.css # Desktop platform-specific button features
mobile.blocks/
button/
button.css # Mobile platform-specific button features
build过程会自动按层级引入,例如:
@import(common.blocks/button/button.css); /* Generic CSS rules */
@import(desktop.blocks/button/button.css); /* Desktop platform-specific */
六.build
BEM项目都有多级文件结构,build完成的工作是:
把Block相关的单独文件整合起来
按需引入Block、Element和Modifier
确保按正确顺序引入(保证Redefinition level)
当然,需要预先定义一些依赖信息,比如:
创建页面时列出相关的Block、Element和Modifier
指明页面中的Block、Element和Modifier的依赖关系
依赖声明可以通过工具完成,比如能够自动根据HTML中应用的class,生成BEM Tree,或者根据项目结构生成依赖信息(非按需生成,所有东西都会被包进来,适用于确定项目目录下所有东西都是必须的情况,例如类库),关于依赖声明方式的更多信息,请查看Declarations in BEM
依赖引入方案与实现有关,比如,CSS用@import
,JS用AMD等方案
build结果
// build前
blocks/ # Directory containing the blocks
bundles/ # Directory containing all build results
hello/ # Directory for the hello page
hello.decl.js # List of BEM entities required for the hello page
An example of a post-build file structure of a BEM project:
// build后
blocks/ # Directory containing the blocks
bundles/ # Directory containing all build results
hello/ # Directory for the hello page
hello.decl.js # List of BEM entities required for the hello page
hello.css # Built CSS file for the hello page
hello.js # Built JavaScript file for thehello page
上例中hello.decl.js
中定义了依赖的组件及其依赖关系
build tool
官方提供的build工具是ENB,据说很强大:
The BEM platform uses ENB, which is a tool suitable for building BEM projects of any level of complexity.
想要尝试请查看新手教程:Starting your own BEM project