42
Компонентный JavaScript* *С помощью Twitter Flight Interlabs 6 мая 2014 1 / 42

Компонентный JavaScript с Twitter Flight

  • View
    729

  • Download
    5

Embed Size (px)

DESCRIPTION

Компонентная архитектура front-end основанная на обмене событиями. Twitter Flight — простая и гибкая реализация архитектуры. Обектная модель основанная на примесях (mixins). Особенности проектирования приложения: структура компонент и именование событий. DOM-шаблонизация как разумный компромисс между работой с DOM вручную и полноценными JavaScript-шаблонизаторами. Преимущества Flight для legacy-кода и разработки сайтов.

Citation preview

Page 1: Компонентный JavaScript с Twitter Flight

Компонентный JavaScript**С помощью Twitter Flight

Interlabs

6 мая 2014

1 / 42

Page 2: Компонентный JavaScript с Twitter Flight

О чем речь

• использование AMD — это хорошо, но мало• jQuery оставляет слишком много места для спагетти-кода• нужна простая архитектура без излишних абстракций• которую можно начать использовать без глобальногопереписывания приложения

• и чтобы не изобретать велосипед

Наверное, нужен какой-нибудь фреймворк?Спасибо, Кэп

2 / 42

Page 3: Компонентный JavaScript с Twitter Flight

JS фреймворкиBackBone ExtJS AngularJS CujoJS

Montage CanJS React Flight Ember.js MarionetteYUI Polymer Dojo KnockoutJS PureMVC . . .

http://todomvc.com/

• большая часть — для одностраничных приложений• часто — вещь в себе, все приложение должно подчинятьсятребованиям фреймворка

• некоторые предлагают собственную объектную модель

3 / 42

Page 4: Компонентный JavaScript с Twitter Flight

Что хотим от фреймворка• возможность использования как для сайтов, так и дляодностраничных приложений

• легкость изучения, прозрачность работы, минимум магии• отсутствие проблем с интеграцией с AMD, jQuery,плагинами jQuery (традиционный стек)

• чем меньше размер, тем лучше• возможность постепенного внедрения в ужесуществующих приложениях

• минимум давления на разработчика

Есть такой фреймворк!4 / 42

Page 5: Компонентный JavaScript с Twitter Flight

FlightAn event-driven web framework, from Twitter1

1http://twitter.github.io/flight/

Page 6: Компонентный JavaScript с Twitter Flight

Коротко о Twitter FlightПроект ЦРУ Twitter, используется в Twitter, TweetDeck, Gumroad,Airbnb, Crashalytics, Mixam . . .

1 приложение = набор компонентов2 компоненты = AMD-модули3 взаимодействие компонентов — только события4 ООП — только композиция, без наследования5 Простая реализация на базе DOM, никаких чудес6 Разработчик волен использовать любые удобныеинструменты для всего остального: шаблоны, роутинг,пользовательский интерфейс и т.д.

Продолжаем использовать jQuery, только правильно6 / 42

Page 7: Компонентный JavaScript с Twitter Flight

Архитектура, основанная на событиях

Идея не нова: минимизируем связанность компонент,полностью исключая их непосредственное взаимодействие.

• компонент никогда не вызывает метод другого компонента• компонент может подписаться на событие и обработать его• компонент может инициировать событие• компонент ничего не знает о других компонентах

7 / 42

Page 8: Компонентный JavaScript с Twitter Flight

Идея не нова

Event Driven Architecture.Publish/Subscribe Pattern.

• реализована в виде библиотек и jQuery-плагинов, Flight —наиболее удобная (для нас) реализация

• хорошо известная и часто рекомендуемая архитектура

Читать: Building Decoupled Large-scale Applications UsingJavaScript (And jQuery)2

2http://bit.ly/1fm2CmE8 / 42

Page 9: Компонентный JavaScript с Twitter Flight

«Их всего два вида»Интерфейсные компоненты

• элемент интерфейса, взаимодействующий с пользователем• привязаны к DOM-элементу, устанавливают обработчикисобытий для него и вложенных элементов

Компоненты данных (сервисы)

• предоставление данных интерфейсным компонентам• привязаны к document

Виды отличаются только выполняемой функцией, с точкизрения реализации они абсолютно одинаковы.

9 / 42

Page 10: Компонентный JavaScript с Twitter Flight

Именование событий

Сложный вопрос, как и вообще naming things:

• вся функциональность описывается событиями• не должно возникать проблем по мере роста приложения• события — постоянная часть приложения, составкомпонент — переменная

• структура компонент не должна отражаться на структуреимен событий

• нет фильтрации по именам событий — нет ограничений наструктуру имен

10 / 42

Page 11: Компонентный JavaScript с Twitter Flight

Именование событийПодход используемый в 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

Page 12: Компонентный JavaScript с Twitter Flight

Пробуем быть проще:namespace1.namespace2...︸ ︷︷ ︸

пространство имен

: descriptive− event − name︸ ︷︷ ︸описательное имя события

• пространства имен группируют события пофункциональным модулям (но не по компонентам)

• имена событий описательны и не используют специальныхпрефиксов, типы событий не всегда однозначны

• единый стиль именования (прошедшее время и т.д.)

shop.cart:request-data shop.cart:datashop.cart:add-item shop.cart:remove-item

12 / 42

Page 13: Компонентный JavaScript с Twitter Flight

Объектная модель: 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

Page 14: Компонентный JavaScript с Twitter Flight

Примеси: базовая семантика“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

Page 15: Компонентный JavaScript с Twitter Flight

Параметризация примесей“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

Page 16: Компонентный JavaScript с Twitter Flight

Примеси FlightФреймворк добавляет композицию, оптимизацию иавтоматическое связывание, нам осталось только написать код:

define(function() {return WithSomething;

function WithSomething() {this.method = function () {// do something

}}

]);

16 / 42

Page 17: Компонентный JavaScript с Twitter Flight

Компоненты FlightКомпонент — просто композиция примесей:

define([’flight/component’,’app/mixin’

], function(component, withMixin) {

// withMixin, myComponent — примесиreturn component(myComponent, withMixin);

function myComponent() {this.method = function() {};

}});

17 / 42

Page 18: Компонентный JavaScript с Twitter Flight

Атрибуты компонента• состояние = набор атрибутов, доступных через 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

Page 19: Компонентный JavaScript с Twitter Flight

Доступ к DOM• можно просто использовать jQuery• можно использовать метод select() для доступа ковложенным элементам

• если селектор соответствует имени атрибута, используетсязначение атрибута

• результат выполнения — объект jQuery

this.defaultAttrs({buttonSelector: ’.js-cart-button’

});...this.select(’buttonSelector’) ...

19 / 42

Page 20: Компонентный JavaScript с Twitter Flight

Модификация методовКаждый метод может быть изменен в стиле AOP:

this.before(methodName, callback);

this.after(methodName, callback);

this.around(methodName, function (originalMethod) {// maybe call

originalMethod();});

Порядок примесей в определении компонента важен!

20 / 42

Page 21: Компонентный JavaScript с Twitter Flight

Инициализация компонента

• выполняется путем модификации встроенного методаinitialize()

• использование before() редко имеет смысл• при использовании after() компонент уже подключен кDOM-элементу

this.after(’initialize’, function () {this.on(’click’, ...);

});

21 / 42

Page 22: Компонентный JavaScript с Twitter Flight

Обработка событийОбработка событий настраивается при инициализации:

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

Page 23: Компонентный JavaScript с Twitter Flight

Обработка событий

// Добавление обработчика:this.on([selector], eventName, callback);

this.on(eventName, {attributeKey: callback

});

// Удаление обработчика:this.off([selector], eventName, [callback]);

Вторая форма on использует делегирование и может бытьиспользована для динамически добавляемых элементов.

23 / 42

Page 24: Компонентный JavaScript с Twitter Flight

Параметры событийthis.addToCart = function(event, data) {

// data сформирован фреймворкомvar $button = data.el;

}this.highlight(event, data) {

// data сформирован другим компонентом}

• event — стандартный jQuery• data.el — элемент, к которому привязано событие длясобытий браузера (например, кнопка, на которую нажали)

• при формировании data в пользовательском событиивсегда лучше создать дополнительный объект-контейнер

24 / 42

Page 25: Компонентный JavaScript с Twitter Flight

Инициирование событийthis.trigger(’shop:cart-changed’, {items: this.items,total: this.totalSum

});

• объект data лучше рассматривать как универсальныйконтейнер

• каждая единица данных — отдельный элемент контейнера• дополнительная информация вычисляется на сторонесервиса (total)

• часто имеет смысл как-то обозначить отправителя, если ихможет быть несколько, например, указав id

25 / 42

Page 26: Компонентный JavaScript с Twitter Flight

Создание компонентаКомпонент создается и привязывается к определенномуDOM-элементу (или к document, если визуальноепредставление отсутствует).

require([

’app/ui/cart’, // - визуальный компонент корзины’app/data/cart’ // - сервис данных корзины

], function(cartUI, cartData) {

cartUi.attachTo(’.js-ui-cart’); // - визуальный элемент — к divcartData.attachTo(document); // - сервис — просто к документу

)};

26 / 42

Page 27: Компонентный JavaScript с Twitter Flight

Структура приложения

• разбиение страницы на компоненты сложнее, чем этоможет показаться

• набор событий должен быть привязан кфункциональности, а не к особенностям реализации

• компоненты должны быть атомарны, вложенныекомпоненты — теоретически возможно, но не надо

• общая функциональность компонент — через примеси,избавляет от вложенности компонент

• иногда уместнее сделать несколько отдельных компонентвместо одного сложного компонента

27 / 42

Page 28: Компонентный JavaScript с Twitter Flight

Пример: интернет-магазин

Группа 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

Page 29: Компонентный JavaScript с Twitter Flight

Пример: интернет-магазин

js/app/shop/cart/ Компоненты корзиныdata/ - сервисы корзиныcart.js - сервис данных корзины

ui/ - UI-компоненты корзиныcart.js - визуальное представление корзины

catalog/ Компоненты каталога товаров:ui/ - UI-компоненты каталога товаров:products.js - список товаровproduct.js - информация о товаре

mixin/ Примесиwith-cart.js - кнопка «Добавить в корзину»

29 / 42

Page 30: Компонентный JavaScript с Twitter Flight

Магазин — события

Относятся к функциональности корзины, не привязаны кструктуре компонентов:

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

Page 31: Компонентный JavaScript с Twitter Flight

Магазин: особенности• есть соблазн сделать каждый товар в списке отдельнымкомпонентом, но помним об атомарности

• везде где есть динамика (список товаров корзины, списоктоваров при AJAX) используем делегирование событий

• кнопку «добавить в корзину» реализуем в виде примеси:работает один и тот же код и в списке, и в описании товара

• каждый компонент, включающий примесь with-cartподписан на обновление списка товаров в корзине исоответственно подсвечивает кнопки

• любой другой компонент может подписаться наshop.cart:data, например, можно выводитьдинамическое сообщение о скидке при достиженииопределенной суммы и т.д.

31 / 42

Page 32: Компонентный JavaScript с Twitter Flight

Магазин: 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

Page 33: Компонентный JavaScript с Twitter Flight

Магазин: 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

Page 34: Компонентный JavaScript с Twitter Flight

Модель• сервисы данных инкапсулируют доступ к данным, в случаесайта достаточно функциональности jQuery

• представление данных — чем проще, тем лучше, сложныетипы данных усложняют обмен между компонентами

• расширяем набор встроенных средств JavaScript:

Lo-Dash7

• вспомогательные функции для работы с массивами,объектами и т.д.

• коллекции: map(), reduce(), all(), any(), forEach() и т.д.• микрошаблоны

7http://lodash.com/34 / 42

Page 35: Компонентный JavaScript с Twitter Flight

Представление

• DOM + jQuery — для простых случаев• полноценный шаблонизатор — хорошо дляодностраничных приложений, для сайтов — проблемаунификации серверных и клиентских шаблонов

• DOM-шаблонизатор — хороший вариант для сайтов.

DOM-шаблонизация

• нет проблем интеграции с серверной частью• не нужна компиляция шаблонов• хорошая производительность

35 / 42

Page 36: Компонентный JavaScript с Twitter Flight

DOM-шаблонизация

Самый простой вариант: Transparency.js8

• отдельные элементы DOM привязываются к данным черезdata-атрибуты или имена классов

• поддержка вывода коллекций через клонирование идобавление фрагментов DOM

• поддержка произвольных манипуляций DOM путемуказания директив, можно использовать jQuery дляотдельных элементов

• быстрая работа, малый размер

8https://github.com/leonidas/transparency36 / 42

Page 37: Компонентный JavaScript с Twitter Flight

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

Page 38: Компонентный JavaScript с Twitter Flight

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

Page 39: Компонентный JavaScript с Twitter Flight

Представление компонент• помним о необходимости делегирования событий,используем соответствующую форму 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

Page 40: Компонентный JavaScript с Twitter Flight

Итого

• Twitter Flight — рекомендуемые паттерны разработки наjQuery в виде готового фреймворка

• обмен событиями простая в понимании и использованииархитектура, минимизирующая jQuery-спагетти

• архитектура достаточно простая для сайтов и достаточномощная для приложений

• проектирование системы событий — самая важная исложная часть

• не стоит переусложнять модель, особенно для сайтов• DOM-шаблонизация хорошо интегрируется как ссерверной частью, так и с традиционным jQuery-кодом

40 / 42

Page 41: Компонентный JavaScript с Twitter Flight
Page 42: Компонентный JavaScript с Twitter Flight

Что читать

Помимо предыдущих ссылок:

• 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