BEMテクノロジーの完全なスタックに関するゼロからのWebサイト。 Yandexの方法論

先週、 BBCは、メインページの新しいバージョンにYandexで作成されたBEM方法論を使用したと述べました。 この機会に、ワークショップ「 BEMテクノロジーの完全なスタックでゼロからサイトを開発する 」の資料を募集し、プロジェクトでBEMテクノロジーの完全なスタックの使用を開始する方法を説明することにしました。



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つの質問は明らかです。その後、興味深い部分が始まります。







何かを検討する時が







再定義レベル



これはブロック実装のセットです。 プロジェクトには複数のレベルがあり、各レベルでブロックの実装が追加または変更されます。 ブロックの最終実装は、指定された順序ですべてのレベルから順番に収集されます。

プロジェクトの再定義レベルで、スタイル、テンプレート、ブロックの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



モードは、残りのモードの通過のセットと順序を記述します。 この図は、各モードの役割を示しています。



HTMLレイアウトmod



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ツリーを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 .



All Articles