View
729
Download
5
Embed Size (px)
DESCRIPTION
Компонентная архитектура front-end основанная на обмене событиями. Twitter Flight — простая и гибкая реализация архитектуры. Обектная модель основанная на примесях (mixins). Особенности проектирования приложения: структура компонент и именование событий. DOM-шаблонизация как разумный компромисс между работой с DOM вручную и полноценными JavaScript-шаблонизаторами. Преимущества Flight для legacy-кода и разработки сайтов.
Citation preview
Компонентный JavaScript**С помощью Twitter Flight
Interlabs
6 мая 2014
1 / 42
О чем речь
• использование AMD — это хорошо, но мало• jQuery оставляет слишком много места для спагетти-кода• нужна простая архитектура без излишних абстракций• которую можно начать использовать без глобальногопереписывания приложения
• и чтобы не изобретать велосипед
Наверное, нужен какой-нибудь фреймворк?Спасибо, Кэп
2 / 42
JS фреймворкиBackBone ExtJS AngularJS CujoJS
Montage CanJS React Flight Ember.js MarionetteYUI Polymer Dojo KnockoutJS PureMVC . . .
http://todomvc.com/
• большая часть — для одностраничных приложений• часто — вещь в себе, все приложение должно подчинятьсятребованиям фреймворка
• некоторые предлагают собственную объектную модель
3 / 42
Что хотим от фреймворка• возможность использования как для сайтов, так и дляодностраничных приложений
• легкость изучения, прозрачность работы, минимум магии• отсутствие проблем с интеграцией с AMD, jQuery,плагинами jQuery (традиционный стек)
• чем меньше размер, тем лучше• возможность постепенного внедрения в ужесуществующих приложениях
• минимум давления на разработчика
Есть такой фреймворк!4 / 42
FlightAn event-driven web framework, from Twitter1
1http://twitter.github.io/flight/
Коротко о Twitter FlightПроект ЦРУ Twitter, используется в Twitter, TweetDeck, Gumroad,Airbnb, Crashalytics, Mixam . . .
1 приложение = набор компонентов2 компоненты = AMD-модули3 взаимодействие компонентов — только события4 ООП — только композиция, без наследования5 Простая реализация на базе DOM, никаких чудес6 Разработчик волен использовать любые удобныеинструменты для всего остального: шаблоны, роутинг,пользовательский интерфейс и т.д.
Продолжаем использовать jQuery, только правильно6 / 42
Архитектура, основанная на событиях
Идея не нова: минимизируем связанность компонент,полностью исключая их непосредственное взаимодействие.
• компонент никогда не вызывает метод другого компонента• компонент может подписаться на событие и обработать его• компонент может инициировать событие• компонент ничего не знает о других компонентах
7 / 42
Идея не нова
Event Driven Architecture.Publish/Subscribe Pattern.
• реализована в виде библиотек и jQuery-плагинов, Flight —наиболее удобная (для нас) реализация
• хорошо известная и часто рекомендуемая архитектура
Читать: Building Decoupled Large-scale Applications UsingJavaScript (And jQuery)2
2http://bit.ly/1fm2CmE8 / 42
«Их всего два вида»Интерфейсные компоненты
• элемент интерфейса, взаимодействующий с пользователем• привязаны к DOM-элементу, устанавливают обработчикисобытий для него и вложенных элементов
Компоненты данных (сервисы)
• предоставление данных интерфейсным компонентам• привязаны к document
Виды отличаются только выполняемой функцией, с точкизрения реализации они абсолютно одинаковы.
9 / 42
Именование событий
Сложный вопрос, как и вообще naming things:
• вся функциональность описывается событиями• не должно возникать проблем по мере роста приложения• события — постоянная часть приложения, составкомпонент — переменная
• структура компонент не должна отражаться на структуреимен событий
• нет фильтрации по именам событий — нет ограничений наструктуру имен
10 / 42
Именование событийПодход используемый в Twitter:
ui data request запрос данных, uiNeedsTwitterProfileui user action действие пользователя, uiFollowActionui request запрос элемента UI к другим элементам UI,
uiCloseModel
ui moment важное событие UI, uiColumnOptionsShowndata отправка данных, dataTwitterProfile
“I’m not sure we’ve got our event-naming nailed as yet. In fact, ournaming conventions seem to be widely disagreed upon within oursmall team.”3
3https://blog.twitter.com/2013/flight-at-tweetdeck11 / 42
Пробуем быть проще:namespace1.namespace2...︸ ︷︷ ︸
пространство имен
: descriptive− event − name︸ ︷︷ ︸описательное имя события
• пространства имен группируют события пофункциональным модулям (но не по компонентам)
• имена событий описательны и не используют специальныхпрефиксов, типы событий не всегда однозначны
• единый стиль именования (прошедшее время и т.д.)
shop.cart:request-data shop.cart:datashop.cart:add-item shop.cart:remove-item
12 / 42
Объектная модель: mixins• классическое наследование — проблема даже втрадиционных ООП-языках, в JavaScript — тем более
• композиция всегда выгоднее, чем наследование
“And on the 6th day, God created an abundance of TalkingAnimals, that they may be used in JavaScript inheritanceexamples”4
• вообще не используем наследование• компонуем классы из отдельных примесей (mixins)• используем функциональные примеси
4https://bit.ly/1isPuH613 / 42
Примеси: базовая семантика“JavaScript polymorphism is probably one of the best things you can findout there.5”
var augmented = function() { // это примесьthis.method = function() {// do something
}}
augmented.call(object);object.method() // применение к отдельному объекту
augmented.call(ObjectClass.prototype);object = new ObjectClass();$object.method(); // применение к классу
5http://webreflection.blogspot.ru/2013/04/flight-mixins-are-awesome.html 14 / 42
Параметризация примесей“This functional strategy also allows the borrowed behaviours to beparameterized by means of an options argument. it6”
var withMessage = function (options) {this.message = function () {console.debug(options.prefix + this.message);
}return this;
}
withMessage.call(SomeClass.prototype, { prefix: ’Hello, ’ });
s = new SomeClass(’world’);s.message(); // Hello, world!
6http://bit.ly/1fxThmN15 / 42
Примеси FlightФреймворк добавляет композицию, оптимизацию иавтоматическое связывание, нам осталось только написать код:
define(function() {return WithSomething;
function WithSomething() {this.method = function () {// do something
}}
]);
16 / 42
Компоненты FlightКомпонент — просто композиция примесей:
define([’flight/component’,’app/mixin’
], function(component, withMixin) {
// withMixin, myComponent — примесиreturn component(myComponent, withMixin);
function myComponent() {this.method = function() {};
}});
17 / 42
Атрибуты компонента• состояние = набор атрибутов, доступных через this.attrs• если продекларированы через defaultAttrs(), могутбыть переопределены при создании компонента
define([’flight/component’], function(component) {return component(myComponent);function myComponent() {this.defaultAttrs({ // Декларация атрибутов:
buttonSelector: ’.js-cart-button’,defaultText: ’Loading cart’
});}this.update = function (event, data) {
this.select(’buttonSelector’).text(this.attrs.defaultText); // - использование
}}
18 / 42
Доступ к DOM• можно просто использовать jQuery• можно использовать метод select() для доступа ковложенным элементам
• если селектор соответствует имени атрибута, используетсязначение атрибута
• результат выполнения — объект jQuery
this.defaultAttrs({buttonSelector: ’.js-cart-button’
});...this.select(’buttonSelector’) ...
19 / 42
Модификация методовКаждый метод может быть изменен в стиле AOP:
this.before(methodName, callback);
this.after(methodName, callback);
this.around(methodName, function (originalMethod) {// maybe call
originalMethod();});
Порядок примесей в определении компонента важен!
20 / 42
Инициализация компонента
• выполняется путем модификации встроенного методаinitialize()
• использование before() редко имеет смысл• при использовании after() компонент уже подключен кDOM-элементу
this.after(’initialize’, function () {this.on(’click’, ...);
});
21 / 42
Обработка событийОбработка событий настраивается при инициализации:
this.highlight = function (event, data) {// в data — параметры события
}
this.after(’initialize’, function() {this.on(’click’, { // - делегированиеbuttonSelector: this.addToCart // атрибут-селектор
});
// Обработка события сервиса данных:this.on(document, ’shop:cart-changed’, this.highlight);
});
22 / 42
Обработка событий
// Добавление обработчика:this.on([selector], eventName, callback);
this.on(eventName, {attributeKey: callback
});
// Удаление обработчика:this.off([selector], eventName, [callback]);
Вторая форма on использует делегирование и может бытьиспользована для динамически добавляемых элементов.
23 / 42
Параметры событийthis.addToCart = function(event, data) {
// data сформирован фреймворкомvar $button = data.el;
}this.highlight(event, data) {
// data сформирован другим компонентом}
• event — стандартный jQuery• data.el — элемент, к которому привязано событие длясобытий браузера (например, кнопка, на которую нажали)
• при формировании data в пользовательском событиивсегда лучше создать дополнительный объект-контейнер
24 / 42
Инициирование событийthis.trigger(’shop:cart-changed’, {items: this.items,total: this.totalSum
});
• объект data лучше рассматривать как универсальныйконтейнер
• каждая единица данных — отдельный элемент контейнера• дополнительная информация вычисляется на сторонесервиса (total)
• часто имеет смысл как-то обозначить отправителя, если ихможет быть несколько, например, указав id
25 / 42
Создание компонентаКомпонент создается и привязывается к определенномуDOM-элементу (или к document, если визуальноепредставление отсутствует).
require([
’app/ui/cart’, // - визуальный компонент корзины’app/data/cart’ // - сервис данных корзины
], function(cartUI, cartData) {
cartUi.attachTo(’.js-ui-cart’); // - визуальный элемент — к divcartData.attachTo(document); // - сервис — просто к документу
)};
26 / 42
Структура приложения
• разбиение страницы на компоненты сложнее, чем этоможет показаться
• набор событий должен быть привязан кфункциональности, а не к особенностям реализации
• компоненты должны быть атомарны, вложенныекомпоненты — теоретически возможно, но не надо
• общая функциональность компонент — через примеси,избавляет от вложенности компонент
• иногда уместнее сделать несколько отдельных компонентвместо одного сложного компонента
27 / 42
Пример: интернет-магазин
Группа 1
Группа 2
Группа 3
Группа 4
Группа 5
Интернет-магазинГлавная > Товары > Группа 1
Список групп,компонент
130 x 132
Название продуктаMutatas dei mollia utque ventos orbe ensis lege campoque
caesa tuba horrifer aethera ipsa undis summaque tantosanctius tempora formaeque vindice gravitate liquidum eurus
cuncta cura sponte nitidis humanas humanas campoquepluviaque aeris manebat.
Купить
Описание товара,компонент
Кнопка купить,примесь
Рейтинг,примесь
В корзине: 5 ($1000)Корзина,компонент
Списокпродуктов,
Продукт 1
Mundi circumfuso, ab effigiem partim mollia securae omnia radiisrudis matutinis fidem.64 x 64
Купить
Продукт 1
Mundi circumfuso, ab effigiem partim mollia securae omnia radiisrudis matutinis fidem.64 x 64
Продукт 2
Mundi circumfuso, ab effigiem partim mollia securae omnia radiisrudis matutinis fidem.
64 x 64
Продукт 3
Mundi circumfuso, ab effigiem partim mollia securae omnia radiisrudis matutinis fidem.
64 x 64
1 2 3 4 5 6 7 8 9 10 Страничныйнавигатор,примесь
Кнопка купить,примесь
Купить
Купить
Купить
Купить
28 / 42
Пример: интернет-магазин
js/app/shop/cart/ Компоненты корзиныdata/ - сервисы корзиныcart.js - сервис данных корзины
ui/ - UI-компоненты корзиныcart.js - визуальное представление корзины
catalog/ Компоненты каталога товаров:ui/ - UI-компоненты каталога товаров:products.js - список товаровproduct.js - информация о товаре
mixin/ Примесиwith-cart.js - кнопка «Добавить в корзину»
29 / 42
Магазин — события
Относятся к функциональности корзины, не привязаны кструктуре компонентов:
shop.cart:request-data - запрос данных корзиныshop.cart:data - обновление данныхshop.cart:add-item - запрос на добавлениеshop.cart:remove-item - запрос на удалениеshop.cart.item-added - успешное добавлениеshop.cart.item-removed - успешное удалениеshop.cart.clear - запрос на очистку корзины
30 / 42
Магазин: особенности• есть соблазн сделать каждый товар в списке отдельнымкомпонентом, но помним об атомарности
• везде где есть динамика (список товаров корзины, списоктоваров при AJAX) используем делегирование событий
• кнопку «добавить в корзину» реализуем в виде примеси:работает один и тот же код и в списке, и в описании товара
• каждый компонент, включающий примесь with-cartподписан на обновление списка товаров в корзине исоответственно подсвечивает кнопки
• любой другой компонент может подписаться наshop.cart:data, например, можно выводитьдинамическое сообщение о скидке при достиженииопределенной суммы и т.д.
31 / 42
Магазин: with-cart.jsdefine([’jquery’, ’lodash’], function ($, _) {
return withCart;function withCart() {
this.defaultAttrrs({ buttons: ’.js-shop-product-cart’ });this.addToCart = function (event, data) {
var $el = $(data.el);this.trigger(’shop.cart:add-item’, {
item: {id: $el.attr(’data-id’);title: $el.attr(’data-title’),price: $el.attr(’data-price’)
}});
};this.highlight = function (event, data) {
this.select(’buttons’).each(function (index, button) {var $button = $(button);$button.toggleClass(’active’, _.indexOf(data.ids, $button.attr(’data-id’)) > -1);
}};this.after(’initialize’, function () {
this.on(’click’, {buttons: this.addToCart
});this.on(document, ’shop.cart:data’, this.hightlight);
});}
});
32 / 42
Магазин: ui/cart.jsdefine([’flight/component’, ’jquery’, ’transparency’], function (component, $, T) {
return component(cart);function cart() {
this.defaultAttrs({summary: ’.js-shop-cart-size’,button: ’.js-shop-cart-button’,container: ’.js-shop-cart-ui’
});this.remove = function (event, data) {
this.trigger(’shop.cart:remove-item’, {id: $(data.el).attr(’data-id’)
});};this.render = function (event, data) {
this.select(’button’).toggleClass(’active’, data.size > 0);this.select(’container’).render({
summary: data.size > 0 ? data.size + ’, $’ + data.total : ’Корзина пуста’,total: data.total,size: data.size,items: data.items
});};this.on(document, ’shop.cart:data’, this.render);this.on(’click’, { remove: this.remove });
}});
33 / 42
Модель• сервисы данных инкапсулируют доступ к данным, в случаесайта достаточно функциональности jQuery
• представление данных — чем проще, тем лучше, сложныетипы данных усложняют обмен между компонентами
• расширяем набор встроенных средств JavaScript:
Lo-Dash7
• вспомогательные функции для работы с массивами,объектами и т.д.
• коллекции: map(), reduce(), all(), any(), forEach() и т.д.• микрошаблоны
7http://lodash.com/34 / 42
Представление
• DOM + jQuery — для простых случаев• полноценный шаблонизатор — хорошо дляодностраничных приложений, для сайтов — проблемаунификации серверных и клиентских шаблонов
• DOM-шаблонизатор — хороший вариант для сайтов.
DOM-шаблонизация
• нет проблем интеграции с серверной частью• не нужна компиляция шаблонов• хорошая производительность
35 / 42
DOM-шаблонизация
Самый простой вариант: Transparency.js8
• отдельные элементы DOM привязываются к данным черезdata-атрибуты или имена классов
• поддержка вывода коллекций через клонирование идобавление фрагментов DOM
• поддержка произвольных манипуляций DOM путемуказания директив, можно использовать jQuery дляотдельных элементов
• быстрая работа, малый размер
8https://github.com/leonidas/transparency36 / 42
Transparency.js: пример
<div class="js-shop-cart"><button><b data-bind="summary">Загрузка корзины...</b></button><table class="js-shop-cart-table">
<tbody data-bind="items"><tr>
<td data-bind="title"></td><td data-bind="qty"></td><td data-bind="price"></td><td><button data-bind="remove">X</button></td>
</tr></tbody>
</table></div>
37 / 42
Transparency.js: примерfunction formatPrice(price) { return ’$’ + price; }T.render(cart, { // - данные
summary: data.size > 0? data.size + ’ item(s) - $’ + data.total : ’Корзина пуста’,
total: data.total,size: data.size,items: data.items // - массив, итерация по элементам
}, { // Директивы:items: { // Для каждого элемента items
price: { // - форматируем текстtext: function() { return formatPrice(this.price); }
},remove: { // - изменяем DOM
html: function (target) {$(target.element).attr(’data-id’, this.id);
}}
},total: { // - форматируем текст
text: function() { return formatPrice(this.price); }}
});38 / 42
Представление компонент• помним о необходимости делегирования событий,используем соответствующую форму on()
• data-атрибуты — удобный способ интеграции ссерверными шаблонами, используем для инициализациикомпонент
• работаем с data-атрибутами через attr(), не забываемпреобразовывать типы, если необходимо
<div class="product js-product"data-id="<?= $product[’id’] ?>"data-title="<?= $product[’title’] ?>"data-price="<?= $product[’price’]"><div class="media-body"><h4><?= $product[’title’] ?></h4>...
</div></div>
39 / 42
Итого
• Twitter Flight — рекомендуемые паттерны разработки наjQuery в виде готового фреймворка
• обмен событиями простая в понимании и использованииархитектура, минимизирующая jQuery-спагетти
• архитектура достаточно простая для сайтов и достаточномощная для приложений
• проектирование системы событий — самая важная исложная часть
• не стоит переусложнять модель, особенно для сайтов• DOM-шаблонизация хорошо интегрируется как ссерверной частью, так и с традиционным jQuery-кодом
40 / 42
Что читать
Помимо предыдущих ссылок:
• Developing a Twitter Flight Edge9 — книга (online)• Getting started with Twitter Flight10 — еще книга• Discover Flight Components11 - репозиторий компонентов• Twitter Flight Google Groups12 — список рассылки
9http://bit.ly/1rV7fnX10http://bit.ly/1muO8zF11http://flight-components.jit.su/12bit.ly/1kOwiYU
42 / 42