BEM开发模式

写在前面

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。日常开发就是写这样的配置,功能、样式等都封装在组件里

具体编译过程是这样:

  1. pageName.bemjson.js声明每个页面的HTML结构以及数据模型

  2. 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_closeyes状态时,执行一个左边收起的动画(没错,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响应状态变更)的一种变体,类似于中转站,如下:

event channel

event channel

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自身也是一个Blockblock : '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生成的HTML结构

BEM生成的HTML结构

如果能够带来高可维护性,长一点丑一点麻烦一点又有什么关系呢?

而且如BEM所说,他们只提供一种方法论,我们可以根据实际情况随便修改(比如微信团队用的就是改良版,详见参考资料)

或者即便不使用BEM,其中的很多原则也是通用的可用的,比如“组件开发中保证依赖最小化”、“禁止直接访问DOM元素”、“4种组件交互方式”等等

参考资料

BEM开发模式》上有1条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code