Изоморфный БЕМ

Коли з'явився node.js багато web-розробники стали замислюватися про можливості використовувати один і той же код на клієнті, так і на сервері. Зараз існує кілька фреймворків, ставлять підхід «пишемо код один раз, використовуємо скрізь» у главу кута, час від часу з'являються нові. Ось і я не зміг пройти мимо, пишу подібний мікро-фреймворк — bnsf. Він призначений для тих, хто воліє створювати front-end своїх додатків за БЕМ-методології, користуючись відповідним набором технологій і інструментів.

Давайте спробуємо почати писати front-end для простого односторінкового web-додатки, використовуючи bnsf. Щоб не відволікатися на створення back-end частини, будемо використовувати в якості back-end'a API vk.com. Наше додаток буде складатися всього з двох сторінок, головною з формою пошуку користувачів по ідентифікатору — і вторинної, на ній будемо виводити інформацію про обраному користувача.

Для початку роботи вам потрібно node.js, yeoman і gulp. Рекомендую використовувати *nix OS, так як під Windows код не тестувався, хоча, теоретично, має працювати. Я виходжу з припущення, що node.js у вас вже встановлений. Якщо це не так, раджу скористатися nvm.

Встановлюємо gulp, yeoman і відповідний генератор:

npm install-g gulp yo generator-bnsf

Створюємо наш проект:

yo bnsf vk-test-app 
cd vk-test-app

Можна подивитися, які файли і папки сгенерировались:

ls

Виведе приблизно такий набір файлів (порядок може відрізнятися на різних операційних системах):

README.md desktop.blocks gulpfile.js node_modules
bower.json desktop.bundles libs package.json

Проект вже можна спробувати зібрати:

gulp

gulp не тільки збере проект, але ще й запустить сервер, почне стежити за змінами в проекті і при необхідності його перезбирати.

Перевіримо, що все працює. Пробуємо відкрити в браузері http://localhost:3000 — ми повинні побачити сторінку з текстом page-index і заголовком main page.

Одна сторінка у нас вже є, давайте створимо другу, для виведення записів зі стіни користувача. Для цього нам знову знадобиться генератор. Оскільки він працює з командного рядка, вам знадобиться ще одна термінальна сесія, щоб не переривати gulp. На цьому етапі можна просто погодитися з усім, про що буде питати yeoman. Він буде попереджати про конфлікти — це стандартна практика, коли файл не створюється новий, а редагується існуючий, так що просто натискайте enter у відповідь на всі питання yo. Отже, виконаємо з кореня проекту:

yo bnsf:page user

Ще раз нагадаю, на всі питання відповідаємо згодою — тобто тиснемо enter.

gulp повинен помітити появу нової сторінки і перезібрати проект. Перевіряємо: запит на http://localhost:3000/user повинен віддати сторінку з текстом page-user.

Давайте тепер розмістимо на головній сторінці форму пошуку, відредагувавши файл
desktop.blocks/page-index/page-index.bemtree
наступним чином:

block('page-index')(
content()(function () {
return [
{
block: 'search-form',
content: [
{
block: 'input',
mods: {
theme: 'простий'
}
},
{
block: 'button',
mods: {
type: 'submit',
theme: 'простий'
},
content: 'search'
}
]
},
{
block: 'search-results'
}
];
})
);

block('page-index').elem('title').content()('main page');

І змінимо відповідно в залежності
page-index.deps.js:


({
mustDeps: ['i-page'],
shouldDeps: [
{ elem: 'title' },
'search-form',
{
block: 'input',
mods: { тему: 'простий' }
},
{
block: 'button',
mods: { тему: 'простий' }
},
'search-results'
]
})

Зараз форма вже виводиться (можна перевірити, знову зайшовши на http://localhost:3000), тільки не тег
form
, а
div
. Щоб це виправити, створимо відповідний файл шаблону,
desktop.blocks/search-form/search-form.bemhtml
:

block('search-form').tag()('form');

Зараз може здатися надмірною створювати окрему директорію з файлом, який зберігає всього один рядок коду. Але в реальному проекті зустріти таке практично неможливо: обов'язково з'являється або файл зі стилями, або з JavaScript, або сам шаблон блоку більш складний. Найчастіше — все вищеперелічене відразу.

Відмінно, у нас є форма, але вона поки не вміє нічого шукати. Нехай «шукати» з точки зору форми — це перенаправляти на поточну сторінку з параметром запиту. Щоб форма почала це робити, знадобиться наступний JS у файлі
desktop.blocks/search-form/search-form.browser.js
:

/**@module search-form*/
modules.define('search-form', ['i-bem__dom', 'app-navigation'], function (provide, BEMDOM, navigation) {
"use strict";

/**
* @class SearchForm
* @extends BEM.DOM
* @exports
*/
provide(BEMDOM.decl(this.name, /**@lends SearchForm#*/{

onSetMod: {
js: {
/**
* @constructs
* @this SearchForm
*/
inited: function () {
this._input = this.findBlockInside('input');
}
}
},

/**
* @param {Event} e
* @private
*/
_onSubmit: function (e) {
e.preventDefault();
var query = this._input.getVal(),
params = query ? {query: query} : null;
navigation.navigate('page-index', params);
}

}, /**@lends SearchForm*/{
/**
* @static
*/
live: function () {
var init = { modName: 'js', modVal: 'inited' };
this
.liveInitOnBlockInsideEvent(init, 'button')
.liveInitOnBlockInsideEvent(init, 'input')
.liveBindTo('submit', function (e) {
this._onSubmit(e)
});
}
}));
});

Доведеться також трохи ускладнити шаблон, додавши до неї інформацію, що у блоку є логіка, файл
desktop.blocks/search-form/search-form.bemhtml
:

block('search-form')(
tag()('form'),
js()(true)
);

Отже, тепер у нас є форма, здатна змінювати get-параметр сторінки. У цьому можна переконатися, ввівши, скажімо, «1» в текстовий инпут і натиснувши enter. Прийшов час отримувати якісь дані по цьому параметру. Я не хочу використовувати API, що вимагає аутентифікації, тому скористаюся методом, доступним кому завгодно url
http://api.vk.com/method/users.get
. Нехай форма приймає ідентифікатор користувача, а виводитися посилання на його сторінку на сторінку user, яку ми створили вище) і на сторінки ще 4-х користувачів з ідентифікаторами, отриманими простим інкрементом. В якості тексту посилань будемо використовувати імена користувачів.

Перше, що нам потрібно зробити — додати маршрут у файл з конфігурацією маршрутів API. Це файл
desktop.bundles/index/index.api.routing.yml
, і ось яким має вийти його вміст:

host: api.vk.com
routes:
- id: користувачі
path: /method/users.get

Друге — Створимо файл
desktop.blocks/search-results/search-results.bemtree
. Основна думка така: кому дані треба відображати, той за ними і ходить. У нашому випадку дані потрібні блоку search-results, йому за даними і йти:

block('search-results').content()(function () {
if (!this.route.parameters.query) {
return ";
}
var id = parseInt(this.route.parameters.query, 10);
return id ? this.get('users', { // відправляємо запит на маршрут сервера API з ідентифікатором user
user_ids: [id, id + 1, id + 2, id + 3, id + 4]
}, function (data) { // в цій функції обробляємо результати запиту
return data.body.response.map(function (dataItem) {
return {
block: 'search-results',
elem: 'item',
content: {
block: 'link',
url: path('page-user', { id: dataItem.uid }), // генеруємо url ідентифікатора маршруту додатки page-user
content: dataItem.first_name + '' + dataItem.last_name
}
};
});
}) : 'Something goes wrong';
});

У цьому шаблоні даних ми дивимося, чи прийшов нам id, якщо прийшов — запитуємо дані по маршруту API з ідентифікатором user і параметром user_ids, використовуючи метод get. Якщо id не число — віддаємо рядок 'Something goes wrong'. Оскільки виводити потрібно буде список, а ми любимо семантику, створимо
desktop.blocks/search-results/search-results.bemhtml
:

block('search-results')
.tag()('ul')
.elem('item').tag()('li');

Крім того, нам знадобиться файл для декларації залежностей блоку,
desktop.blocks/search-results/search-results.deps.js
:

({
shouldDeps: ['link']
})

Тепер сторінка вже вміє шукати користувачів і виводити результати. Спробуйте, тільки не забудьте оновити сторінку. Якщо введете «1» — у видачі результатів повинні знайти Павла Дурова. Тільки от біда — перемальовується кожен раз вся сторінка. Це легко виправити, навчивши її оновлювати тільки необхідне. Доповнимо
page-index.bemtree
, щоб він виглядав наступним чином:

block('page-index')(
content()(function () {
return [
{
block: 'search-form',
content: [
{
block: 'input',
mods: {
theme: 'простий'
}
},
{
block: 'button',
mods: {
type: 'submit',
theme: 'простий'
},
content: 'search'
}
]
},
{
block: 'search-results'
}
];
}),
js()({
update: 'search-results' // ми додали конфігурацію для клієнтського JavaScript: ім'я блоку, який слід оновлювати
})
);

block('page-index').elem('title').content()('main page');

Тепер, відкривши інспектор в браузері, можна переконатися, що за нових запитах до API оновлюється тільки блок search-results.

Ну що ж, прийшла пора зайнятися другою сторінкою, не дарма ж ми її створювали.
Почнемо з
desktop.blocks/page-user/page-user.bemtree
:

block('page-user').content()(function () {
return [
{
block: 'menu',
content: {
block: 'link',
url: path('page-index'),
content: 'main page'
}
},
{
block: 'user-card'
}
];
});

block('page-user').elem('title').content()('user');

Ми додали фейковий блок меню — просто як обгортку для посилання на головну сторінку, саму посилання і блок user-card, який буде виводити інформацію про користувача.
Не забуваємо оновити залежності
desktop.blocks/page-user/page-user.deps.js
:

({
mustDeps: ['i-page'],
shouldDeps: ['link', 'user-card']
})

Я не додав у залежності блок menu, тому що не збираюся його реалізовувати.

Щоб вивести картку користувача, створимо файл
desktop.blocks/user-card/user-card.bemtree
:

block('user-card').content()(function () {
return this.get('users', {
user_ids: this.route.parameters.id
}, function (data) {
return data.body.response.map(function (dataItem) {
var output = [];
for (var key in dataItem) {
if (dataItem.hasOwnProperty(key)) {
output.push({
elem: 'рядок',
content: [
{
elem: 'key',
content: key
},
{
elem: 'value',
content: JSON.stringify(dataItem[key])
}
]
});
}
}
return output;
});
});
});

В такому вигляді вже буде працювати. Можна спробувати натиснути на посилання в результатах пошуку, тільки не забудьте перед цим оновити сторінку, щоб підтягнути новий код. Але давайте зробимо картку користувача таблицею, визначивши
desktop.blocks/user-card/user-card.bemhtml
:

block('user-card')(
tag()('table'),
elem('row').tag()('tr'),
elem('key').tag()('td'),
elem('value').tag()('td')
);

Ось так набагато краще.

Думаю, на цьому пора закінчити, хоча можна було б ще додати як мінімум валідацію користувальницького введення, більш акуратні url, показ процесу завантаження, повернення до останнього пошуку… Залишу це на домашнє завдання тим, хто зацікавився. Ну або на наступну статтю, якщо зацікавлені попросять про такий.

Корисні посилання:
bnsf — фреймворк, про який йдеться у статті. Насправді просто бібліотека блоків в термінології БЕМ.
bem-core — бібліотека блоків, від якої залежить bnsf
bem-components — бібліотека блоків, яка використовується в проекті, створеному вище
bem.info — сайт про bem з документацією, зокрема, там можна почитати про:
bemtree — технологію для побудови вхідних даних для шаблонизатора за даними від API і
bemhtml — декларативний шаблонизатор
Стаття в тему by Nickolas Zackas. переклад.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.