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 .

Source: https://habr.com/ru/post/J251473/


All Articles