メインコンテンツへ移動

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 は 1 つのページを記述し、コンパイルを経て 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)と大差なく、无非は 2 つのことを説明:

  • HTML 構造

  • データ装入ルール

deps

package 依存に類似。以下の通り:

({
    mustDeps: [],
    shouldDeps: [
        { block: 'link' },
        { block: 'box' }
    ]
})

同様に、依存設定は build 時に依存コンポーネントがすでに導入されていることを確保するためのものです

三.ロジック階層

一連のコンポーネントが 1 つのロジック階層を形成し、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 は 2 種類のイベントを提供し、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 を暫定保存、すべての消費者がこの item を pull するまで

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 自身も 1 つの 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 が言うように、彼らは 1 つの方法論のみを提供し、私たちは実際状況に応じて随便に修正できます(例えば微信チームが使用しているのは改良版で、詳細は参考資料を参照)

または BEM を使用しなくても、その中の多くの原則は通用的で利用可能です。例えば「コンポーネント開発中で依存を最小化することを保証」、「DOM 要素への直接アクセスを禁止」、「4 種類のコンポーネントインタラクション方式」など

参考資料

コメント

コメントはまだありません

コメントを書く