BEM(Block-Element-Modifier)

一.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

参考资料

发表评论

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

*

code