BEMは、迅速に作成して長期間維持する必要があるサイトの開発を簡素化します。 このテクノロジーは、ほぼすべてのYandexサービスのフロントエンドで使用されており、すでに多くのライブラリとツールを取得しています。
この記事では、独立したブロックのレイアウトの利点とオーバーライドレベルについて説明し、既製のブロックライブラリとアセンブリを自動化するツールについて説明します。 autoprefixer 、css-preprocessor Stylus 、モジュラーシステムYModuleなどのさまざまなツールが開発者の生活をどのように簡素化し、BEM開発プロセスに組み込むと本当に便利なプラットフォームを作成するかを示します。
生きた例を使用して、CSSとJavaScriptの両方に同じアイデアを使用できる場合、宣言型アプローチの使用方法を説明します。 宣言型テンプレートBEMHTMLおよびBEMTREEについて個別に説明します。これらのテンプレートにより 、データをBEMJSON形式で記述されたHTMLで記述されたBEMツリーに変換できます。 BEM方法論に従って、アプリケーションのサーバー部分を作成する方法を詳細に検討してみましょう。
Twitter APIを使用してプロジェクトを作成します。 その結果、BEMテクノロジーの完全なスタックに関する作業サイトと、これらすべてを再現する方法に関する段階的な記事を入手できます。
特にマスタークラスについては、さまざまなソーシャルネットワークを検索し、結果を整然とした形式で表示するミニサービスを作成しました。 github.com/bem/sssrリポジトリのgithubに投稿しました-を参照してください 。
そして順番に行きます。
理論
BEMとは
BEM(単語の略語-ロック、要素、および修飾子)は、特定の実装テクノロジに関連付けられていないエンティティを記述する方法であるプログラムとインターフェイスを開発するための方法論です。
- ブロックは、アプリケーションの独立したコンポーネントです。 他のブロックから独立しており、他のブロックや要素を含む場合があります。
- 要素は、個別の機能を担当するブロックの一部です。 ブロックから分離しても意味がありません。
- 修飾子は、その外観または動作を担当するブロックまたは要素のプロパティです。 修飾子は、ブロックまたは要素の状態を記述します。
BEMは、DOMツリーを抽象化します。 ブロックは互いに独立しており、すべての機能と要素をカプセル化しています。 ブロックが実装されるHTMLタグ(
div
または
form
)は重要ではありません。これをいつでも変更したり、ラッパーを追加したりできます。 変更が残りのブロックに影響することはありません。 HTMLタグではなく、インターフェイスコンポーネントを使用してアプリケーションを説明します。
各ブロックは、ファイルシステム内の独自のフォルダーにあり、ブロック、その要素、および修飾子を記述するすべてのテクノロジーが含まれています。
desktop.blocks/ input/ __box/ # __clear/ # __control/ # _focused/ # _type/ # input.css # css input.js # js input.ru.md # markdown …
BEMがどのように、なぜ登場したかについて詳しく知りたい場合は、Vitaly Kharisovの記事「 BEMの歴史 」を読み、 ビデオレポートをご覧ください。
BEM方法論の詳細な説明は、 当社のWebサイトにあります。
プロジェクトブランクの作成
仕事に必要なものをすべてインストールします。
まず、ターミナルと
git
バージョン管理システムが必要です。 git-scm.comからインストールできます。
ほとんどすべてのツールはJavaScriptで記述されているため、 node.jsまたはio.jsが必要になります。
プロジェクトのブランクを作成するには、 generator-bem-stub generatorを使用します。
> npm install -g generator-bem-stub
次に、ジェネレーター自体を実行します。
> yo bem-stub
使用されている技術に関する質問に答えると、組み立てのために事前に組み立てられ、構成されたブランクが得られます。
次の問題を調べてみましょう。
スクリーンショットは、質問に対する回答の結果を示しています。 最初の3つの質問は明らかです。その後、興味深い部分が始まります。
-
Choose a toolkit to build the project
(使用するコレクター) -ENBツールを使用します。 これは、プロジェクトが組み立てるユーティリティです。スタイル、スクリプト、テンプレートを接着し、ページ宣言、ブロックの依存関係、構成ファイルに従ってコンパイルおよび最適化します。 -
Specify additional libraries if needed
します(追加のライブラリを使用しますか)-このプロジェクトでは、 bem-componentsブロックライブラリを使用します。 オプションのスタイルテーマがあります。
何かを検討する時が
。
再定義レベル
これはブロック実装のセットです。 プロジェクトには複数のレベルがあり、各レベルでブロックの実装が追加または変更されます。 ブロックの最終実装は、指定された順序ですべてのレベルから順番に収集されます。
プロジェクトの再定義レベルで、スタイル、テンプレート、ブロックのJavaScript実装を再定義および再定義できます。 同時に、ライブラリのソースファイルには何も変更しないため、更新された場合は変更を保存できます。
これがファイルシステムでどのように見えるかの例を次に示します。
… libs/ bem-components/ desktop.blocks/ input/ input.css desktop.blocks/ input/ input.css …
プロジェクトの
desktop.blocks
レベルでブロックを作成することにより、必要なテクノロジーをオーバーライドまたはオーバーライドできます。
上記の例では、
CSS
テクノロジーに実装を追加することにより、
input
ブロックのスタイルを編集できます。
これで、ドラフトプロジェクトの準備ができました。 プロジェクトディレクトリに移動しましょう。
> cd sssr-tutorial
レイアウト
まず、ページの静的プロトタイプを作成します。 その構造を説明するために、 BEMJSONテクノロジーを使用しています。
BEMJSONは、BEMツリーを説明します。ブロックの順序とネスト、BEMエンティティの名前と状態、追加の任意のフィールド。
生成されたプロジェクトを収集して、何が起こったのか見てみましょう。 ローカルにインストールされた
ENB
パッケージを使用して便利に作業するには、次のコマンドを実行する必要があります。
> export PATH=./node_modules/.bin:$PATH
または、。
./node_modules/.bin/
サブディレクトリから手動で
enb
コマンドを実行します
ビルドするには、
enb server
コマンドを使用し
enb server
。
> enb server
http:// localhost:8080 / desktop.bundles / index / index.html。コレクターは、必要なすべての依存関係を収集し、それらから必要なブロックとテクノロジーのファイルを収集します。
ブラウザでインスペクターを開き、DOMツリーを確認します。 まだコードを記述していませんが、このページには既にHTMLが生成されています。 これは、ライブラリのテンプレートが使用されているためです。 たとえば、
bem-core
ライブラリの
page
ブロックテンプレートは、ページ
doctype
(
doctype
、
html
、
head
、
body
など)を生成します。
プロジェクトの
./desktop.bundles/index/
フォルダーに
index.bemjson.js
ファイルが含まれて
./desktop.bundles/index/
ます。
({ block: 'page', title: 'Hello, World!', styles: [ { elem: 'css', url: 'index.min.css' } ], scripts: [ { elem: 'js', url: 'index.min.js' } ], content: [ 'Hello, World!' ] })
このファイルは、BEM用語でのページの説明です。 BEMツリーのルートブロックは
page
です。
favicon
追加キーワード-
title
、
favicon
などがあります。 このブロックのテンプレートはbem-coreライブラリにあります。
このアプリケーションは、キャップとコンテンツという2つの主要部分で構成されています。
sssr
ブロックをページコンテンツに追加します。このコンテンツでは、インターフェイスの一部が要素として記述されます。 これを行うには、.
./desktop.bundles/index/index.bemjson.js
/
./desktop.bundles/index/index.bemjson.js
/
./desktop.bundles/index/index.bemjson.js
編集し
./desktop.bundles/index/index.bemjson.js
。
({ block: 'page', //… content: [ { block: 'sssr', content: [ { elem: 'header' }, { elem: 'content' } ] } ] });
ヘッダーには、検索フォームとロゴ付きのサイト名が含まれます。
{ block: 'sssr', content: [ { elem: 'header', content: [ { elem: 'logo', content: 'Social Services Search Robot:' }, { block: 'form', content: [ { elem: 'search' }, { elem: 'filter', content: '[x] twitter' } ] } ] }, { elem: 'content' } ] }
bem-components
ライブラリの
input
、
button
、
spin
、および
checkbox
ブロックを使用し
input
。 プロジェクトでは、このライブラリは
./libs/bem-components
フォルダーにあります。 これらの各ブロックには独自のAPIがあり、API はドキュメントに記載されています 。
BEMJSONに必要なブロックを追加します。
{ block: 'sssr', content: [ { elem: 'header', content: [ { elem: 'logo', content: [ { block: 'icon', mods: { type: 'sssr' } }, 'Social Services Search Robot:' ] }, { block: 'form', content: [ { elem: 'search', content: [ { block: 'input', mods: { theme: 'islands', size: 'm', 'has-clear' : true }, name: 'query', val: '#b_', placeholder: 'try me, baby!' }, { block: 'button', mods: { theme: 'islands', size: 'm', type: 'submit' }, text: '' }, { block: 'spin', mods: { theme: 'islands', size : 's' } } ] }, { elem: 'filter', content: '[] twitter [] instagram' } ] } ] } ] }
mods
フィールドはこのBEMJSONスニペットにあります。 使用される修飾子とその意味を示します。
mods
フィールドには
:
-
mods: { type: 'sssr' }
ます。
BEMJSONで任意のJavaScript式を使用できます。
checkbox
ブロックを繰り返すための
map
コンストラクトを
filter
要素の
content
フィールドに追加し
map
。
//… { elem: 'filter', content: ['twitter', 'instagram'].map(function(service) { return { block: 'checkbox', mods: { theme: 'islands', size: 'l', checked: service === 'twitter' }, name: service, text: service }; }) } //…
完全な
index.bemjson.js
ファイル:
({ block: 'page', title: 'Social Services Search Robot', favicon: '/favicon.ico', head: [ { elem: 'meta', attrs: { name: 'description', content: 'find them all' }}, { elem: 'css', url: '_index.css' } ], scripts: [{ elem: 'js', url: '_index.js' }], content: { block: 'sssr', content: [ { elem: 'header', content: [ { elem: 'logo', content: [ { block: 'icon', mods: { type: 'sssr' } }, 'Social Services Search Robot:' ] }, { block: 'form', content: [ { elem: 'search', content: [ { block: 'input', mods: { theme: 'islands', size: 'm', 'has-clear' : true }, name: 'query', val: '#b_', placeholder: 'try me, baby!' }, { block: 'button', mods: { theme: 'islands', size: 'm', type: 'submit' }, text: '' }, { block: 'spin', mods: { theme: 'islands', size : 's' } } ] }, { elem: 'filter', content: ['twitter', 'instagram'].map(function(service) { return { block: 'checkbox', mods: { theme: 'islands', size: 'l', checked: service === 'twitter' }, name: service, text: service }; }) } ] } ] }, { elem: 'content' } ] } })
インターフェイスの構造を説明した後、ブロックのスタイルを記述および再定義する必要があります。 すべての主要なスタイルは、
bem-components
ライブラリで提供されます。 そのため、かなり追加する必要があります。
Stylus CSSプリプロセッサを使用してスタイルを記述します。
*.styl
を持つすべてのファイルは、プリプロセッサによって処理され、最終的なCSSファイルに接着されます。 プリプロセッサで処理する必要のないスタイルには、
*.css
拡張子を使用することもでき
*.css
。
ファイル
./desktop.blocks/form/form.styl
form
ブロックのスタイルを
./desktop.blocks/form/form.styl
します。
.form { display: flex; &__search { margin-right: auto; } .input { width: 400px; } .checkbox { display: inline-block; margin-left: 15px; user-select: none; vertical-align: top; } }
./desktop.blocks/page/page.css
ファイルの
page
ブロックの場合:
.page { font-family: Tahoma, sans-serif; min-height: 100%; margin: 0; padding-top: 100px; background: #000; }
ファイル
./desktop.blocks/sssr/sssr.styl
sssr
ブロックの
./desktop.blocks/sssr/sssr.styl
:
.sssr { &__header { position: fixed; z-index: 1; top: 0; box-sizing: border-box; width: 100%; padding: 10px 10%; background: #f6f6f6; box-shadow: 0 0 0 1px rgba(0,0,0,.1), 0 10px 20px -5px rgba(0,0,0,.4); .button { margin-left: 10px; } } &__logo { font-size: 18px; margin: 0 0 10px; } &__content { padding: 10px 10%; column-count: 4; column-gap: 15px; transition: opacity .20s linear; } a[rel='nofollow'], a[xhref], [name][server] { text-decoration: none; color: #038543; } }
そして、
user
ブロック
desktop.blocks/user/user.styl
:
.user { &__name { display: inline-block; margin-right: 10px; text-decoration: none; color: #000; &:hover { text-decoration: underline; color: #038543; } } &__post-time { font-size: 14px; display: inline-block; color: #8899a6; } &__icon { position: absolute; right: 5px; bottom: 5px; width: 30px; height: 30px; border-radius: 3px; } }
CSSレイアウトの問題については詳しく説明しません。先に進みましょう。
見つかったメッセージにブロックを追加することは残ります。 それらを
index.bemjson.js
説明し、JavaScript機能を使用してプロトタイプを作成します。
{ elem: 'content', content: (function() { return 'BEM is extermly cool'.split('').map(function() { var service = ['twitter', 'instagram'][Math.floor(Math.random()*2)]; return { service: service, user: [{ login: 'tadatuta', name: 'Vladimir', avatar: 'https://raw.githubusercontent.com/bem/bem-identity/master/sign/_theme/sign_theme_batman.png' }, { login: 'dmtry', name: 'Dmitry', avatar: 'https://raw.githubusercontent.com/bem/bem-identity/master/sign/_theme/sign_theme_captain-america.png' }, { login: 'sipayrt', name: 'Jack Konstantinov', avatar: 'https://raw.githubusercontent.com/bem/bem-identity/master/sign/_theme/sign_theme_ironman.png' }, { login: 'einstein', name: 'Slava', avatar: 'https://raw.githubusercontent.com/bem/bem-identity/master/sign/_theme/sign_theme_robin.png' }][Math.floor(Math.random()*4)], time: Math.floor((Math.random()*12)+1) + 'h', img: service === 'instagram' ? 'http://bla.jpg' : undefined, text: [ ' — . ( ).', ' — .', ' .'][Math.floor(Math.random()*3)] }; }).map(function(dataItem) { return { block: 'island', content: [ { elem: 'header', content: { block: 'user', content: [ { block: 'link', mix: { block: 'user', elem: 'name' }, url: 'https://www.yandex.ru', target: '_blank', content: dataItem.user.name }, { elem: 'post-time', content: dataItem.time }, { block: 'image', mix: { block: 'user', elem: 'icon' }, url: dataItem.user.avatar, alt: dataItem.user.name } ] } }, { elem: 'text', content: dataItem.text }, { elem: 'footer', content: [ { block: 'service', mods: { type: dataItem.service } } ] } ] }; }); })() }
island
ブロックのスタイルを
./desktop.blocks/island/island.styl
ファイルに
./desktop.blocks/island/island.styl
ます。
.island { font-size: 18px; line-height: 140%; position: relative; display: inline-block; box-sizing: border-box; width: 100%; margin-bottom: 15px; padding: 15px 5px 5px 15px; border-radius: 3px; background: #fff; box-shadow: inset 0 0 1px rgba(0, 0, 0, .4); &__footer { margin-top: 10px; } &__image { display: block; width: 100%; border-radius: 3px; } }
結果を見てみましょう:
BEMHTMLテンプレートエンジン
宣言的な標準化
Yandexは宣言性が非常に好きです-CSSだけでなく、テンプレートとJavaScriptでも。
CSSの宣言性は次のようになります。
.menu__item { display: inline-block; }
menu
display: inline-block;
すべての
item
要素に対して
display: inline-block;
スタイルが適用され
display: inline-block;
、つまり 処理方法を宣言します
条件によって選択されたDOMノード:
{ }
条件に一致するDOMツリーのすべてのノードを選択し、テンプレート本体をそれらに適用します。
宣言的なテンプレート化のために、Yandexは独自のBEMHTMLテンプレートエンジンを作成しました。 アーキテクチャの詳細については、記事bem-coreのデータテンプレートを参照してください。
BEMHTMLの宣言型テンプレートの例:
block('menu').elem('item').tag()('span');
条件に一致するすべてのBEMツリーブロックが選択され、テンプレート本体がそれらに適用されます。
()( )
BEMHTMLはJavaScriptで記述されています。 その構文は純粋なJavaScriptです。 JavaScript関数をサブ述語とテンプレートの本文で使用できます。 プロダクションモードの場合、テンプレートは最適化されたJavaScriptにコンパイルされます。
BEMHTMLは、BEMツリーをHTML文字列に変換する方法を担当します。 入力は、BEMツリーまたはBEMJSONテクノロジで記述されたそのフラグメントです。 このBEMJSONは、BEMHTMLテンプレートに重ねられます。 そして、出力はHTML文字列です。
一般的に、テンプレートは次のとおりです。
match(1, 2, 3)();
サブレディケートは、パターンが適用される条件です。 例:
match(1, 2, 3)();
このテンプレートは、現在のブロックが
link
ブロックであるか、
this.ctx
のコンテキストに
url
変数があるかどうか、および現在のモードが
tag
modであるかどうかを
this.ctx
ます。 これらのすべての条件が満たされると、タグがブロックに適用されます。
ファッション
ファッションは、HTML出力を生成するステップです。 各modは、結果のHTMLコードの独自の部分を担当します。
default
モードは、残りのモードの通過のセットと順序を記述します。 この図は、各モードの役割を示しています。
BEMHTMLテンプレートエンジンリファレンスガイドに記載されているBEMHTMLドキュメントを注意深く読むことをお勧めします。
プロジェクトに戻りましょう。
form
ブロックが必要です。
<form>
として表示され、
JavaScript
実装が必要です。
このようなブロックをページにもう1つ追加する場合、BEMJSONファイルでこれらのパラメーターを直接編集する必要があります。 これは、HTMLでのインラインスタイルの使用に似ています。 すべてを正しく行い、ブロックパラメーターをテンプレートに入れましょう。
./desktop.blocks/form/form.bemhtml
:
block('form')( tag()('form'), js()(true) );
これで、ブロックテンプレートを1か所で編集し、このブロックを簡単に転送して再利用できます。
インスペクターでDOMツリーを見てみましょう-
form
ブロックは、
i-bem
クラスの
<form>
として表示されるようになりました。 このクラスは、ブロックにJavaScriptの実装があることを示しています。
BEMブロックをHTMLに変換する方法について説明しました。 それでは、twitterデータがどのように受信され、処理されるかを見てみましょう。
アプリケーションアーキテクチャ
2段階の標準化
アプリケーションは次のように機能します。
- 最初の段階では、サービスからデータを収集し、このデータに基づいてBEMツリーを構築します。
- 2番目では、BEMツリー(ビュー指向データ)をDOMツリーに変換し、HTMLをクライアント側にレンダリングします。
ベントレー
BEMツリーをHTMLに変換する方法について説明しました。 これはフロントエンドサーバーのタスクです。 また、BEMTREEテンプレートエンジンは、BEMツリーを構築してデータで飽和させるタスクを処理します。 BEMHTMLと同じ構文です。 主な違いは、利用可能な標準modの数です。 BEMTREEには、
default
と
content
のみがあり
default
。
BEMTREEへの入力は、ブロックテンプレートが飽和している生データです。 出力では、BEMツリーの既製のフラグメントを取得し、BEMHTMLテンプレートに渡します。
戦いに直行します。
{ type: 'twitter' }
修飾子、
island
ブロックのBEMTREEテンプレートを書きましょう:
desktop.blocks/island/_type/island_type_twitter.bemtree
block('island').mod('type', 'twitter').content()(function() { var data = { postLink: '#', userName: 'user@name', userNick: 'user@nick', createdAt: '19 of July', avatar: '#avatar', text: 'message going here', type: 'twitter' }; return [ { elem: 'header', content: { block: 'user', content: [ { block: 'link', mods: { theme: 'islands' }, mix: { block: 'user', elem: 'name' }, url: data.postLink, content: [data.userName, ' @', data.userNick] }, { elem: 'post-time', content: data.createdAt.toString() }, { block: 'image', mix: { block: 'user', elem: 'icon' }, url: data.avatar, alt: data.userName } ] } }, { elem: 'text', content: data.text }, { elem: 'footer', content: [ { block: 'service', mods: { type: data.type } } ] } ]; });
このブロックのコンテンツに必要なパラメーターを指定して
image
ブロックを転送し、
island
ブロックの
image
要素を混合します。
将来、静的オブジェクトをテンプレートに渡されるデータに置き換えます。 しかし、最初に、サーバーコードの編成方法と、このデータの送信方法を見てみましょう。
サーバー上
このアプリケーションは、 エクスプレスフレームワークで動作します-検索クエリに応答してHTMLをレンダリングします。
サービスからデータを収集するブロックを作成します。
*.node.js
持つファイルにサーバーコードを記述します。これは、アセンブリ中に1つのファイルに接着されます。
node.js
を使用して起動し
node.js
service_type_twitter
ブロック
twitterでの作業を簡単にするために、 twitモジュールを使用します。
npm
を使用してインストールします。
> npm i twit --save
twitterでの作業に必要な認証データは、 別のファイルに入れます 。 同じ名前のファイルでその内容を自分自身にコピーします。
./desktop.blocks/service/_type/service_type_twitter.node.js
編集し
./desktop.blocks/service/_type/service_type_twitter.node.js
。
var twitter = require('twit'), config = require('./service_type_twitter.config'), twit = new twitter(config); var query = '#b_', results = []; twit.get('search/tweets', { q: query, count: 20 }, function(err, res) { if (err) { console.error(err); return []; } results = res.statuses.map(function(status) { var user = status.user; return { avatar: user.profile_image_url, userName: user.name, userNick: user.screen_name, postLink: 'https://twitter.com/' + user.screen_name, createdAt: status.created_at, text: status.text, type: 'twitter' }; }); console.log(results); });
このアプリケーションは、キーワード
#b_
を検索し、結果をコンソールに表示します。
プロジェクトを再構築し、
node.js
を使用して実行します
> enb make > node ./desktop.bundles/index/index.node.js
実行の結果は、コンソールのツイートのリストになります。
ここで、さらなる作業のために、実行の結果を何らかの方法で転送する必要があります-標準化とクライアントへの転送。
promiseを使用した非同期作業には、 vowライブラリを使用します。
サーバーとクライアントのJSコードの構成-モジュラーシステムYModules 。
モジュラーシステム
bem-core
ライブラリは、モジュラーシステムymodulesを使用します。
ブロックのコードをモジュールラッパーにラップし、必要に応じて他のモジュールから呼び出すことができます。
次の追加に従って
service_type_twitter.node.js
ファイルを編集します。
modules.define('twitter', function(provide) { var vow = require('vow'), moment = require('moment'), twitter = require('twit'), twitterText = require('twitter-text'), config = require('./service_type_twitter.config'), twit = new twitter(config); provide({ get: function(query) { var dfd = vow.defer(); twit.get('search/tweets', { q: query, count: 20 }, function(err, res) { if(err || !res.statuses) { console.error(err); dfd.resolve([]); } dfd.resolve(res.statuses.map(function(status) { return { avatar: status.user.profile_image_url, userName: status.user.name, userNick: status.user.screen_name, postLink: 'https://twitter.com/' + status.user.screen_name, createdAt: moment(status.created_at), text: twitterText.autoLink(twitterText.htmlEscape(status.text)), type: 'twitter' }; })); }); return dfd.promise(); } }); });
ご覧のとおり、すべてのコードを
modules.define
コンストラクトにラップしました。 これは
twitter
モジュール宣言であり、
modules
名前空間を介してアプリケーションで後で利用可能になります。
結果の非同期転送では、クエリの結果に応じて、エラーが発生した場合は空の配列、または検索結果の配列を渡すプロミスを返します。
日付を処理するには、モジュール
moment.js
追加します。
Twitterはメッセージでプレーンテキストを返すため、
twitter-text
ライブラリを使用してハッシュタグとリンクを強調表示します。
さらに、上記のように、
express
が必要になります。
これらのモジュールをインストールしましょう。
> npm i vow moment twitter-text express --save
server
ブロック
サーバーブロックは、アプリケーションのサーバー側の操作を担当します。 フォルダー
./desktop.blocks/server/
を追加し、その中に
server.node.js
ファイルを作成します。
これは、URL
/search
をリッスンし、要求に応じてデータを返す
express
アプリケーションになります。
modules.require(['twitter'], function(twitter) { var fs = require('fs'), PATH = require('path'), express = require('express'), app = express(), url = require('url'), querystring = require('querystring'), Vow = require('vow'); app.get('/search', function(req, res) { var dataEntries = [], searchObj = url.parse(req.url, true).query, queryString = querystring.escape(searchObj.query), servicesEnabled = []; searchObj.twitter && servicesEnabled.push(twitter.get(queryString)); Vow.all(servicesEnabled) .then(function(results) { res.end(JSON.stringify(results, null, 4)); }) .fail(function() { console.error(arguments); }); }); var server = app.listen(3000, function() { console.log('Listening on port %d', server.address().port); }); });
次の内容で
./desktop.blocks/sssr/sssr.deps.js
ファイルを作成します。
({ shouldDeps: [ { block: 'server' }, { block: 'island', mods: { type: ['twitter'] }} ] })
ここでは、
sssr
ブロックが
server
するために
type: 'twitter'
修飾子を持つ
server
ブロックと
island
ブロックが必要であると述べてい
server
。
server
ブロックに応じて
service_type_twitter
修飾子も追加し
server
。 これを行うには、ファイル
./desktop.blocks/server/server.deps.js
作成します。
({ shouldDeps: [ { block: 'service', mods: { type: ['twitter'] } }, { block: 'sssr', } ] })
これで、必要なすべてのブロックがアセンブリに分類されます。 プロジェクトを再構築し、サーバーを起動します。
> enb make && node ./desktop.bundles/index/index.node.js
アドレスhttp:// localhost:3000 / search?Query =%23b_&twitter = onで、JSONデータオブジェクトを含むページが開き、
service_type_twitter
ブロックが提供します。
次に、BEMTREEを使用して、このデータのBEMJSONへの変換を追加します。
server.node.js
:
modules.require(['twitter'], function(twitter) { var fs = require('fs'), PATH = require('path'), VM = require('vm'), express = require('express'), app = express(), url = require('url'), querystring = require('querystring'), moment = require('moment'), Vow = require('vow'), pathToBundle = PATH.join('.', 'desktop.bundles', 'index'); app.use(express.static(pathToBundle)); var bemtreeTemplate = fs.readFileSync(PATH.join(pathToBundle, 'index.bemtree.js'), 'utf-8'); var context = VM.createContext({ console: console, Vow: Vow }); VM.runInContext(bemtreeTemplate, context); var BEMTREE = context.BEMTREE; app.get('/search', function(req, res) { var dataEntries = [], searchObj = url.parse(req.url, true).query, queryString = querystring.escape(searchObj.query), servicesEnabled = []; searchObj.twitter && servicesEnabled.push(twitter.get(queryString)); Vow.all(servicesEnabled) .then(function(results) { // , // Object.keys(results).map(function(idx) { dataEntries = dataEntries.concat(results[idx]); }); // dataEntries.sort(function(a, b) { return b.createdAt.valueOf() - a.createdAt.valueOf(); }); // BEMJSON BEMTREE BEMTREE.apply(dataEntries.map(function(dataEntry) { dataEntry.createdAt = moment(dataEntry.createdAt).fromNow(); return { block: 'island', data: dataEntry, mods: { type: dataEntry.type } }; })) .then(function(bemjson) { // JSON res.end(JSON.stringify(bemjson, null, 4)); }); }) .fail(function() { console.error(arguments); }); }); var server = app.listen(3000, function() { console.log('Listening on port %d', server.address().port); }); });
BEMTREE- ,
vow
, .
, , .
BEMTREE.apply()
, , - , BEMTREE-.
./desktop.blocks/island/_type/island_type_twitter.bemtree
:
block('island').mod('type', 'twitter').content()(function() { var data = this.ctx.data; return [ // ]; });
this.ctx.data
,
BEMTREE.apply()
.
http://localhost:3000/search?query=%23b_&twitter=on . BEMJSON, BEMTREE.
BEMJSON HTML
BEMHTML.apply()
. server.node.js :
var BEMHTML = require(PATH.join('../../' + pathToBundle, 'index.bemhtml.js')).BEMHTML; //… BEMTREE.apply(dataEntries.map(function(dataEntry) { dataEntry.createdAt = moment(dataEntry.createdAt).fromNow(); return { block: 'island', data: dataEntry, mods: { type: dataEntry.type } }; })) .then(function(bemjson) { if (searchObj.json) { return res.end(JSON.stringify(bemjson, null, 4)); } res.end(BEMHTML.apply(bemjson)); }); //…
, HTML, — AJAX.
json=on
— BEMJSON- — http://localhost:3000/search?query=%23b_&twitter=on&json=on .
JavaScript i-bem.js
JavaScript JavaScript- - - –
i-bem.js
.
bem-core
.
i-bem.js
—
i-bem
js
.
jQuery
API .
, i-bem.js .
:
- ;
- ;
- .
js-
js-, . , , js-, BEMHTML
js
, BEMJSON —
js
:
// bemhtml block('form').js()(true);
// bemjson { block: 'form', js: true }
// bemjson with js params { block: 'form', js: { p1: 'v1', p2: 'v2' } }
js
, , js- . HTML:
<div class="form i-bem" data-bem="{form: {p1: 'v1', p2 : 'v2'}}"></div>
i-bem
, DOM- js-. -
data-bem
, js-, — , .
js
form
./desktop.blocks/form/form.js
:
modules.define('form', ['i-bem__dom'], function(provide, BEMDOM) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this.bindTo('submit', this._onSubmit); } } }, _onSubmit: function(e) { e.preventDefault(); this.emit('submit'); }, getVal: function() { return this.domElem.serialize(); } })); });
bem-core
.
i-bem
— .
i-bem__dom
— , DOM .
form
,
i-bem__dom
, DOM-.
BEMDOM
.
form
. ,
js
inited
—
i-bem.js
. ,
_onSubmit
, ,
getVal
, .
_onSubmit()
e.preventDefault()
, -
submit
, . API
form
. -.
sssr
, .
./desktop.blocks/sssr/sssr.js
:
modules.define('sssr', ['i-bem__dom', 'jquery'], function(provide, BEMDOM, $) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this.findBlockInside('form').on('submit', this._sendRequest, this); } } }, _sendRequest: function() { $.ajax({ type: 'GET', dataType: 'html', cache: false, url: '/search/', data: this.findBlockInside('form').getVal(), success: this._onSuccess.bind(this) }); }, _onSuccess: function(html) { BEMDOM.update(this.elem('content'), html); } })); });
.
sssr
i-bem__dom
, DOM-,
jquery
AJAX.
submit
form
.
_sendRequest
, AJAX-. ,
_onSuccess
,
sssr__content
.
,
i-bem.js
,
sssr
js-:
// desktop.blocks/sssr/sssr.bemhtml block('sssr').js()(true);
, , .
index.node.js
:
$ enb make && node ./desktop.bundles/index/index.node.js
. localhost:3000 , - , . , .
, .
borschik
.
.borschik
:
{ "freeze_paths": { "libs/**": ":base64:", "libs/**": ":encodeURIComponent:" } }
production
:
> YENV=production enb make && node desktop.bundles/index/index.node.js
.
. spin
- , . , «».
spin
, . BEMJSON-.
bem-components
API. :
modules.require(['jquery'], function($) { $('.spin').bem('spin').setMod('visible'); });
spin_visible
true
.
,
js
- .
./desktop.blocks/sssr/sssr.styl
:
.sssr { .spin { margin-left: 1em; vertical-align: middle; } }
, .
./desktop.blocks/sssr/sssr.js
:
modules.define('sssr', ['i-bem__dom', 'jquery'], function(provide, BEMDOM, $) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this.findBlockInside('form').on('submit', this._doRequest, this); } }, loading: function(modName, modVal) { console.log('visible: ', modVal); this.findBlockInside('spin').setMod('visible', modVal); } }, // … _doRequest: function() { this.setMod('loading'); this._sendRequest(); }, _onSuccess: function(html) { this.delMod('loading'); BEMDOM.update(this.elem('content'), html); } })) })
JS-, CSS- . , , .
./desktop.bundles/sssr/sssr.styl
:
.sssr { .spin { margin-left: 1em; vertical-align: middle; } &_loading .content { opacity: 0.5; } }
: localhost:3000 .
spin
, — .
, , .
isEmpty()
:
./desktop.blocks/form/form.js
:
isEmpty: function() { return !this.findBlockInside('input').getVal().trim() || this.findBlocksInside('checkbox').every(function(checkbox) { return !checkbox.hasMod('checked'); }); }
input
checkbox_checked
.
, ,
sssr
:
./desktop.blocks/sssr/sssr.js
:
modules.define('sssr', ['i-bem__dom', 'jquery'], function(provide, BEMDOM, $) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this.findBlockInside('form').on('submit', this._doRequest, this); } }, loading: function(modName, modVal) { this.findBlockInside('spin').setMod('visible', modVal); } }, _doRequest: function() { if (this.findBlockInside('form').isEmpty()) { return; } this.setMod('loading'); this._sendRequest(); }, _sendRequest: function() { //… })
_doRequest()
.
, , .
_sendRequest()
clear()
_updateContent()
.
./desktop.blocks/sssr/sssr.js
:
modules.define('sssr', ['i-bem__dom', 'jquery'], function(provide, BEMDOM, $) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this.findBlockInside('form').on('submit', this._doRequest, this); } }, loading: function(modName, modVal) { this.findBlockInside('spin').setMod('visible', modVal); } }, _doRequest: function() { if (this.findBlockInside('form').isEmpty()) { return; } this.setMod('loading'); this._sendRequest(); }, clear: function() { this._xhr && this._xhr.abort(); this._updateContent(''); this.delMod('loading'); }, _sendRequest: function() { this._xhr && this._xhr.abort(); this._xhr = $.ajax({ type: 'GET', dataType: 'html', cache: false, url: '/search/', data: this.findBlockInside('form').getVal(), success: this._onSuccess.bind(this) }); }, _onSuccess: function(result) { this.delMod('loading'); this._updateContent(result); }, _updateContent: function(html) { BEMDOM.update(this.elem('content'), html); } })); })
, ,
.
form
change
input
:
modules.define('form', ['i-bem__dom'], function(provide, BEMDOM) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this.bindTo('submit', this._onSubmit); this.findBlockInside('input').on('change', this._onChange, this); } } }, _onChange: function() { this.emit('change'); }, // … })
change
sssr
,
./desktop.blocks/sssr.js
:
modules.define('sssr', ['i-bem__dom', 'jquery'], function(provide, BEMDOM, $) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this.findBlockInside('form').on('submit change', this._doRequest, this); } }, // … })); })
,
./desktop.blocks/form.js
:
modules.define('form', ['i-bem__dom'], function(provide, BEMDOM) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this.bindTo('submit', this._onSubmit); this.findBlockInside('input').on('change', this._onChange, this); BEMDOM.blocks.checkbox.on(this.domElem, 'change', this._onChange, this); } } }, // … })
:
. .
debounce
bem-core
.
sssr
sssr.deps.js
:
({ shouldDeps: [ { block: 'server' }, { block: 'island', mods: { type: ['twitter'] }}, { block: 'functions', elem: 'debounce' } ] })
- . ,
functions__debounce
debounce
:
modules.define('sssr', ['i-bem__dom', 'jquery', 'functions__debounce'], function(provide, BEMDOM, $, debounce) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this.findBlockInside('form').on('submit change', this._doRequest, this); this._debounceRequest = debounce(this._sendRequest, 500, this); } }, loading: function(modName, modVal) { this.findBlockInside('spin').setMod('visible', modVal); } }, _doRequest: function(e) { this.setMod('loading'); if (this.findBlockInside('form').isEmpty()) { this._clear(); return; } e.type === 'change' ? this._debounceRequest(): this._sendRequest(); }, _clear: function() { this._xhr && this._xhr.abort(); this._updateContent(''); this.delMod('loading'); }, _sendRequest: function() { this._xhr && this._xhr.abort(); this._xhr = $.ajax({ type: 'GET', dataType: 'html', cache: false, url: '/search/', data: this.findBlockInside('form').getVal(), success: this._onSuccess.bind(this) }); }, _onSuccess: function(result) { this.delMod('loading'); this._updateContent(result); }, _updateContent: function(html) { BEMDOM.update(this.elem('content'), html); } })); });
, . — .
.
sssr
params
.
index.bemjson.js
:
{ block: 'sssr', mods: { autorefresh: true }, js: { url: '/search/', refreshInterval: 10000 }, // ... }
:
./desktop.blocks/sssr/_autorefresh/sssr_autorefresh.js
:
modules.define('sssr', ['tick'], function(provide, tick, Sssr) { provide(Sssr.decl({ modName: 'autorefresh' }, { onSetMod: { loading: function(modName, modVal) { // this.__base.apply(this, arguments); // — , // — modVal ? this._clearTimer(): this._setTimer(); } }, _setTimer: function() { this._counter = 0; tick.on('tick', this._onTick, this); }, _onTick: function() { // (++this._counter * 50) % this.params.refreshInterval || this._sendRequest(); }, _clearTimer: function() { tick.un('tick', this._onTick, this); }, getDefaultParams: function() { return { refreshInterval: 10000 }; } })); });
this.__base
sssr_loading
.
tick
.
tick
50 .
sssr_loading
, , .
refreshInterval
sssr
, .
getDefaultParams
. , .
sssr
.
desktop.blocks/sssr/sssr.deps.js
:
({ shouldDeps: [ 'server', { block: 'functions', elem: 'debounce' }, { block: 'island', mods: { type: ['twitter'] } } ] })
. 10 .
, ,
.
this.findBlockInside('form')
this._form
.
spin
.
sssr
, .
modules.define('sssr', ['i-bem__dom', 'jquery', 'functions__debounce'], function(provide, BEMDOM, $, debounce) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this._spin = this.findBlockInside('spin'); this._form = this.findBlockInside('form') .on('submit', this._doRequest, this); this._debounceRequest = debounce(this._sendRequest, 500, this); } }, loading: function(modName, modVal) { this._spin.setMod('visible', modVal); } }, _doRequest: function(e) { this.setMod('loading'); if (this._form.isEmpty()) { this._clear(); return; } e.type === 'change' ? this._debounceRequest(): this._sendRequest(); }, _clear: function() { this._abortRequest(); this._updateContent(''); this.delMod('loading'); }, _abortRequest: function() { this._xhr && this._xhr.abort(); }, _sendRequest: function() { this._abortRequest(); this._xhr = $.ajax({ type: 'GET', dataType: 'html', cache: false, url: this.params.url, data: this._form.getVal(), success: this._onSuccess.bind(this) }); }, _onSuccess: function(result) { this.delMod('loading'); this._updateContent(result); }, _updateContent: function(result) { BEMDOM.update(this.elem('content'), result); } })); });
, , ,
_abortRequest()
.
遅延初期化
, , .
.
live
-. i-bem.js .
sssr
form
, . :
./desktop.blocks/sssr/sssr.js
:
modules.define('sssr', ['i-bem__dom', 'jquery', 'functions__debounce'], function(provide, BEMDOM, $, debounce) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this._form = this.findBlockInside('form'); this._spin = this.findBlockInside('spin'); this._debounceRequest = debounce(this._sendRequest, 500, this); } }, loading: function(modName, modVal) { this._spin.setMod('visible', modVal); } }, _clear: function() { this._abortRequest(); this._updateContent(''); this.delMod('loading'); }, _doRequest: function(needDebounce) { if (this._form.isEmpty()) { this._clear(); return; } this.setMod('loading'); needDebounce? this._debounceRequest() : this._sendRequest(); }, _sendRequest: function() { this._abortRequest(); this._xhr = $.ajax({ type: 'GET', dataType: 'html', url: this.params.url, data: this._form.getVal(), cache: false, success: this._onSuccess.bind(this) }); }, _abortRequest: function() { this._xhr && this._xhr.abort(); }, _onSuccess: function(result) { this._updateContent(result); this.delMod('loading'); }, _updateContent: function(html) { BEMDOM.update(this.elem('content'), html); } }, { live: function() { this.liveInitOnBlockInsideEvent('submit change', 'form', function(e) { this._doRequest(e.type === 'change'); }); } })); });
live
-
form
:
./desktop.blocks/form/form.js
:
modules.define('form', ['i-bem__dom'], function(provide, BEMDOM) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this._input = this.findBlockInside('input'); this._checkboxes = this.findBlocksInside('checkbox'); } } }, // … isEmpty: function() { return !this._input.getVal().trim() || this._checkboxes.every(function(checkbox) { return !checkbox.hasMod('checked'); }); } }, { live: function() { var ptp = this.prototype; this .liveBindTo('submit', ptp._onSubmit) .liveInitOnBlockInsideEvent('change', 'input', ptp._onChange) .liveInitOnBlockInsideEvent('change', 'checkbox', ptp._onChange); } })); });
input
checkbox
, ,
findBlockInside
.
, -.
i-bem.js
, BEMTREE - BEMHTML - HTML. sssr
service__type__*
API Instagram .. , . , .
, . info@bem.info .