写在前面
BEM是yandex(俄罗斯最大的搜索引擎)实践总结得出的,整套东西看起来很大只,因为官方文档一直强调methodology、the BEM world这种听起来大而空的词,很容易把人吓退
如果由此及彼,从我们普遍接受的理论向BEM映射,会发现BEM没什么神秘的,只是把模块化、工程化的理念与MVC等设计模式结合起来了。他们自称the BEM world,是因为自行构造了一套世界观,想通过文档强加给别人(类似于《天才在左 疯子在右》中的情节),不必太在意
BEM(Block-Element-Modifier)中简单介绍了BEM的理念,提出一堆术语重新定义模块、状态、逻辑分层,目的是尽可能解耦,追求高可维护性与工程化的美,比如:
严格的组件间交互限制(尽量减少Block之间的依赖)
强制统一命名规范(环境级的强约束,开发人员必须遵守)
灵活的逻辑分层(通过Redefinition level)
基于状态的CSS和JS结构(Modifier)
看起来很完美,但存在很多疑问:
Q1.模块加载,打包发布方便吗?
Q2.支持任意多层级的重定义?
Q3.build是可控的吗?
Q4.
body{margin: 0; font-size: 12px;}
这样的base样式放在哪里?Q5.全局逻辑放哪里?
Q6.动态数据怎么处理?(动态修改
page.bemjson.js
?还是动态创建组件?)Q7.跨组件业务怎么实现?
如果要开启BEM模式,必将面临这些问题,接下来的目标就是去同化(由此及彼的映射)BEM,寻求答案
一.页面
从新手教程弄到的test-project
结构如下:
common.blocks/ #库模块
desktop.blocks/ #项目模块
desktop.bundles/ #项目build结果
libs/ #第三方模块
node_modules/
bower.json #用bower管理lib
favicon.ico
package.json
README.md
README.ru.md
其中desktop.bundles/index/index.bemjson.js
是项目首页,没错,不是xxx.html
,页面对应BEM中的BEMJSON,写页面就是写BEMJSON配置,如下:
module.exports = {
block : 'page',
title : 'BEM-组件化',
favicon : '/favicon.ico',
head : [
{ elem : 'meta', attrs : { name : 'viewport', content : 'width=device-width, initial-scale=1' } },
{ elem : 'css', url : 'index.min.css' }
],
scripts: [{ elem : 'js', url : 'index.min.js' }],
content : [{
block : 'header',
content : [
'header is fixed'
]
}, {
block : 'goods',
goods : [{
title: 'Apple iPhone 4S 32Gb',
image: 'http://www.ayqy.net/image/logo.png',
price: '259',
url: 'http://www.ayqy.net/'
},
...]
}, {
block : 'footer',
content : [
'footer content goes here'
]
}]
};
page.bemjson.js
描述一个页面,经编译生成page.html
。日常开发就是写这样的配置,功能、样式等都封装在组件里
具体编译过程是这样:
pageName.bemjson.js
声明每个页面的HTML结构以及数据模型BEMHTML template engine解析BEMJSON生成页面HTML和相关资源
对于模板引擎而言,BEMJSON提供了组件组织结构,也就是所谓的BEMTree
开发页面的过程就是组合使用组件,如果没有或者不合适,可以创建新组件或者在上层重写组件,而每个组件都有严格的约束,保证可复用,这意味着相当高的组件产出率
二.组件
组件是拼装页面的元件,各个组件被隔离在独立的文件目录中,例如:
my-block/
css/styl #CSS文件/stylus文件
js #JS文件
bemhtml.js #定义HTML结构
deps.js #声明依赖项
bemjson.js #描述测试页面,用于单元测试
CSS
CSS命名空间一直是道德约束,BEM把它变成强制规则了,例如:
.goods {
display: -webkit-flex;
display: -moz-flex;
display: -ms-flex;
display: -o-flex;
display: flex;
text-align: center;
padding-left: 0;
}
.goods__item {
-webkit-flex: 1;
-moz-flex: 1;
-ms-flex: 1;
-o-flex: 1;
flex: 1;
list-style: none;
}
.goods__item_new {
background-color: #ff0;
}
写着确实难受,看着也不漂亮,但表意明确,从200行CSS中找到目标行需要多久?2秒就够了,因为你绝对清楚目标行准确的类名,而且不存在子子孙孙多处修改的问题
P.S.B-name__E-name_M-name
规则并不是硬性规定,完全可配置,比如团队决定B_name--E_name-M_name
,这完全没问题
P.S.BEM开发环境默认引入stylus
JS
这里的js不是普通的$('#id').on(...)
,而是基于状态的,由i-bem.js
提供支持,如下:
modules.define('box', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl('box', {
onSetMod : {
'closed': {
'yes': function() {
this.domElem.animate({
'margin-left' : '54em'
}, 1000);
},
'': function() {
this.domElem.css({
'margin-left' : 'auto'
});
}
}
}
}));
});
意思是box_close
为yes
状态时,执行一个左边收起的动画(没错,i-bem__dom
依赖jQuery),box_close
状态不存在时,恢复正常
没有看到$('id')
之类的DOM查找,也没有看到$el.on(...)
之类的DOM Events处理。这样做是为了避免JS直接访问DOM更新视图,前端MVC的基本原则。为了避免随时随地全局DOM查找更新视图引起的组件耦合,i-bem.js
提供了一套受限的DOM API,如下:
// Inside the block — On DOM nodes nested in the DOM node of the current block.
findBlocksInside([elem], block)
findBlockInside([elem], block)
// Outside the block — On DOM nodes that the current block DOM node is a descendent of.
findBlocksOutside([elem], block)
findBlockOutside([elem], block)
// On itself — On the same DOM node where the current block is located. This is relevant when multiple JS blocks are located on a single DOM node (a mix).
findBlocksOn([elem], block)
findBlockOn([elem], block)
BEMHTML
BEMHTML类似于BEMJSON,是用来声明HTML结构的(俗称:模板),例如:
block('goods')(
tag()('ul'),
content()(function() {
return this.ctx.goods.map(function(item){
return [{
elem: 'item',
elemMods: {
new: item.new ? 'yes' : undefined
},
content: [{
elem: 'title',
content: {
block: 'link',
mix: [{block: 'goods', elem: 'link'}],
url: item.url,
content: item.title
}
}, {
block: 'box',
content: {
block: 'image',
url: item.image
}
}, {
elem: 'price',
content: item.price
}]
}];
});
}),
elem('item')(
tag()('li')
),
elem('title')(
tag()('h3')
),
elem('price')(
tag()('span')
)
);
其实是定义了模版,数据定义在页面中(page.bemjson.js
),编译时拼装
与传统的模板(jade, ejs)大同小异,无非说明了两件事情:
HTML结构
数据装入规则
deps
类似于package
依赖,如下:
({
mustDeps: [],
shouldDeps: [
{ block: 'link' },
{ block: 'box' }
]
})
同样的,依赖配置都是为了确保build时已经引入依赖组件
三.逻辑层级
一系列组件形成一个逻辑层级,BEM把这个叫Redefinition level,如下:
common.blocks/
attach/
button/
checkbox/
checkbox-group/
control/
control-group/
dropdown/
icon/
image/
...
每个组件都有独立的目录,common.blocks
就是它们所属的逻辑层
文件夹等于逻辑层?怎么做到的?
非常简单,build时按顺序载入,后来的自然会覆盖先到的,如下:
// .enb/make.js
levels = [
{ path: 'libs/bem-core/common.blocks', check: false },
{ path: 'libs/bem-core/desktop.blocks', check: false },
{ path: 'libs/bem-components/common.blocks', check: false },
{ path: 'libs/bem-components/desktop.blocks', check: false },
{ path: 'libs/bem-components/design/common.blocks', check: false },
{ path: 'libs/bem-components/design/desktop.blocks', check: false },
{ path: 'libs/j/blocks', check: false },
'common.blocks',
'desktop.blocks'
];
这就是逻辑层级的实现,也就是BEM Redefinition level的秘密
组件按文件夹分层级,有内置的common.blocks
,项目自定义的desktop.blocks
,可以在项目级重写内置组件,也可以新添加一级,重写项目级组件
四.组件间交互
上面介绍的组件看起来隔离限制很多(逻辑层级、组件独立目录),如果所有逻辑都分发给组件了,那当然没有问题,但这不可能,总有一些逻辑是需要组件交互的
不想让组件紧耦合,还要让组件之间能交互,那不用想了,肯定是事件机制没错
BEM中关于组件交互的有4条,如下:
BEM Event订阅处理
直接调用其它Block实例的公开方法及Block的静态方法
检测其它Block的状态
event channel
BEM提供了两套事件,DOM Event和BEM Event,对应API不同,分别是bindTo/unbindFrom()
和on/un()
,并从道德角度进行了约束:
不要跨组件使用DOM Event,DOM Event仅在Block内部使用
可以直接调用其它Block的实例方法及静态方法,那怎么才能拿到实例对象?前面有提到的:
// Outside the block — On DOM nodes that the current block DOM node is a descendent of.
findBlocksOutside([elem], block)
findBlockOutside([elem], block)
找到实例后自然可以hasMod/getMod()
检测其状态
最后的event channel是观察者模式(基本结构是Subject改变状态,Observer响应状态变更)的一种变体,类似于中转站,如下:
3条线,如下:
1.生产者new item -> push给event channle -> event channel把该item push给所有消费者;同时暂存item,直到所有消费者都pull过这个item了
2.消费者pull item -> event channel给他
3.event channel轮询所有生产者(pull可以由消费者和event channel发起)
关于event channel的更多信息请查看CS635: Doc 8, Observer Variants(圣地亚哥大学?)
五.enb命令
make
node_modules/.bin/enb make
run a server
node_modules/.bin/enb server
node_modules/.bin/enb server -p portNum
创建css文件
node_modules/bem/bin/bem create -l desktop.blocks -b header -T css
l是redefinition level
b是block
T是implementation technology
在desktop.blocks级重定义库block
node_modules/bem/bin/bem create -l desktop.blocks -b input -T css
node_modules/bem/bin/bem create -l desktop.blocks -b page -T bemhtml.js
page级,修改结构(wrapper),添加样式(与创建css文件方式相同)
node_modules/bem/bin/bem create -l desktop.blocks -b page -T bemhtml.js
node_modules/bem/bin/bem create -l desktop.blocks -b page -T css
同时创建bemhtml和css
node_modules/bem/bin/bem create -l desktop.blocks -b goods -T bemhtml.js -T css
mix组合block
支持2种mix:
mix(block, elem)
mix(block, block)
在组件的bemhtml中定义mix结构,并装入数据
elem: 'title',
content: {
block: 'link',
mix: [{block: 'goods', elem: 'link'}],
url: item.url,
content: item.title
}
也可以在page的BEMJSON中定义
{
block : 'footer',
mix: [{
block: 'box'
}],
content : [
'footer content goes here'
]
}
声明block依赖,确保依赖组件正确引入
node_modules/bem/bin/bem create -l desktop.blocks -b goods -T deps.js
引入第三方库,同样遵循BEM的库
在bower.json中声明依赖
node_modules/.bin/bower i
然后更新make.js
配置重定义层级
.enb/make.js
重写组件js
node_modules/bem/bin/bem create -l desktop.blocks -b box -T js
添加新页面,服务会在第一次访问该页面的时候编译
node_modules/bem/bin/bem create -l desktop.bundles -b about
访问http://localhost:8080/desktop.bundles/about/about.html
六.问题解答
Q1.模块加载,打包发布方便吗?
配置文件按需加载,很少需要手动配置,打包发布有命令行工具,很方便
Q2.支持任意多层级的重定义?
支持
.enb/make.js
Q3.build是可控的吗?
可控,可以修改
.enb/make.js
中的build规则Q4.
body{margin: 0; font-size: 12px;}
这样的base样式放在哪里?body自身也是一个Block:
block : 'page', title : 'BEM-组件化'
,重定义page组件即可,例如.page { margin: 0; font-size: 12px; background-color: #fff; }
Q5.全局逻辑放哪里?
全局逻辑放在page组件里(类似于base样式的方案),可以作为body的一种状态,例如
mods : { map: 'show' }
Q6.动态数据怎么处理?(动态修改
page.bemjson.js
?还是动态创建组件?)动态请求数据,拿到后通过事件机制通知相关Block
Q7.跨组件业务怎么实现?
见组件间交互部分提到的4种方式
七.总结
BEM看起来稍显笨重,但项目每一个文件每一行都条理清晰,可读性非常好
优势:
组件产出率高
写页面就是拼组件,没有组件就随时自定义,组件边界严格,能保证可复用
组件可维护
每个组件都有独立文件夹,边界限定,较难做到与其它组件耦合
性能优势
组件粒度小,按需引入,没有冗余
编码风格统一
样式类名、JS函数名等等都是内置的一套规则,保证每个人代码都长得差不多
按状态控制
模糊事件概念,只关注组件与组件状态,SoC优势
逻辑分离
模块化、逻辑层、MVC把一切尽可能地分解开,更清晰
如果能够带来高可维护性,长一点丑一点麻烦一点又有什么关系呢?
而且如BEM所说,他们只提供一种方法论,我们可以根据实际情况随便修改(比如微信团队用的就是改良版,详见参考资料)
或者即便不使用BEM,其中的很多原则也是通用的可用的,比如“组件开发中保证依赖最小化”、“禁止直接访问DOM元素”、“4种组件交互方式”等等
123