寫在前面
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 響應狀態變更)的一種變體,類似於中轉站,如下:
[caption id="attachment_1137" align="alignnone" width="438"]
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 自身也是一個 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 把一切盡可能地分解開,更清晰
[caption id="attachment_1138" align="alignnone" width="395"]
BEM 生成的 HTML 結構[/caption]
如果能夠帶來高可維護性,長一點醜一點麻煩一點又有什麼關係呢?
而且如 BEM 所說,他們只提供一種方法論,我們可以根據實際情況隨便修改(比如微信團隊用的就是改良版,詳見參考資料)
或者即便不使用 BEM,其中的很多原則也是通用的可用的,比如「組件開發中保證依賴最小化」、「禁止直接訪問 DOM 元素」、「4 種組件交互方式」等等
暫無評論,快來發表你的看法吧