跳到主要內容
黯羽輕揚每天積累一點點

BEM 開發模式

免費2016-09-03#Front-End#前端工程化#前端bem#event channel#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 響應狀態變更)的一種變體,類似於中轉站,如下:

[caption id="attachment_1137" align="alignnone" width="438"]event channel event channel[/caption]

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 把一切盡可能地分解開,更清晰

[caption id="attachment_1138" align="alignnone" width="395"]BEM 生成的 HTML 結構 BEM 生成的 HTML 結構[/caption]

如果能夠帶來高可維護性,長一點醜一點麻煩一點又有什麼關係呢?

而且如 BEM 所說,他們只提供一種方法論,我們可以根據實際情況隨便修改(比如微信團隊用的就是改良版,詳見參考資料)

或者即便不使用 BEM,其中的很多原則也是通用的可用的,比如「組件開發中保證依賴最小化」、「禁止直接訪問 DOM 元素」、「4 種組件交互方式」等等

參考資料

評論

暫無評論,快來發表你的看法吧

提交評論