На конференции EmberConf 2015 Йегуда Кац и Том Дейл сообщили о скором появлении некоторых изменений в Ember 2. В частности, наибольшее внимание привлекли маршрутизируемые компоненты. Они позволяют признать контроллеры устаревшими и убрать их. Конечно, это встревожило многих пользователей Ember, так как Ember и Sproutcore всегда были клиентскими фреймворками MVC.
Прощай MVC
Не секрет, что многие новые соглашения и изменения Ember 2 возникают под непосредственным влиянием React и Flux (паттерн для потока данных в приложениях React). То, что во Flux называют «однонаправленным потоком данных», в Ember является паттерном «данные вниз, действия вверх» (DDAU).
Вместо традиционного паттерна клиентского MVC DDAU предлагает один поток данных (отсюда и направленность в одну сторону), что позволяет проще воспринимать код приложения и улучшать производительность. Фундаментальная проблема с MVC раскрывается по мере роста и усложнения приложения. Каскадное обновление и неочевидные зависимости ведут к неразберихе и путанице. Обновление одного объекта приводит к изменению другого, что, в свою очередь, запускает следующие процессы и в конечном счете превращает поддержку приложения в настоящий «кошмар».
Ember 2.0 Data Down, Actions UP
В паттерне DDAU потоки данных однонаправленные, и здесь нет двухсторонних привязок. Разные части приложения могут оставаться в значительной мере несвязными и понятными. Это значит, что вы всегда будете знать источник изменения объекта. Если вы читали мой пост на тему функционального программирования и эффекта наблюдателя, то вы поймете, почему так важно сохранить независимость компонентов от сторонних эффектов.
Вместо того чтобы тратить время на компоновку самодельного фреймворка с дюжиной микробиблиотек и бессмысленные поиски лучшего способа реализовать какую-либо функцию, стоит использовать Ember, который сразу предлагает производительность и низкий порог вхождения для разработчика. В сочетании с паттерном DDAU и механизмом визуализации Glimmer разработчики Ember изначально имеют в своем распоряжении и производительность и эффективность.
Подготовка приложения для DDAU
Когда появятся маршрутизируемые компоненты, контроллеры устареют и будут убраны из фреймворка. Контроллеры и представления всегда путали новых пользователей Ember, и «в 80 % случаев применения они исполняли роль компонента» (в этом видео от Иегуды и Тома вы сможете узнать больше).
Так как компоненты не являются синглтонами, когда Glimmer посчитает нужным, они будут разобраны и отображены заново в оптимизированной форме. Но если контроллеры уберут, как справиться с сохранением состояния?
Например, у вас в контроллере есть некоторое свойство. Оно удерживает состояние, которое вы хотели бы сохранить в приложении. Чтобы сделать это в Ember 2, мы можем удалить это свойство контроллера и поставить вместо него «компоненты с поддержкой сервисов». Сервис будет сохранять состояние одиночного компонента и непосредственно вводить его, только когда это необходимо. Так как сервисы предоставляют много возможностей, некоторые разработчики могут ими злоупотреблять. О сервисах я скажу в конце статьи.
Реализация компонентов с поддержкой сервисов
В следующем примере, я продемонстрирую, как сегодня можно использовать сервисы и однонаправленные привязки. Вы можете читать текст ниже и параллельно использовать это demo.
Это небольшое приложение состоит из нескольких чекбоксов с выбором животных. Эта выборка должна сохраняться на разных маршрутах и восстанавливать состояние при возвращении к маршруту. Нам нужно определить простой сервис, который сохраняет состояние для выбранных элементов и затем вводит его в маршрутизируемый компонент.
В шаблоне маршрута мы можем просто отобразить введенное состояние сервиса через хелпер each.
{{! animals/index.hbs }}
<div class="row">
<div class="col-md-3">
<h2>Select Animals</h2>
{{checkbox-group
group=animals
selectedItems=checkboxGroup.selectedItems
check=(action "check")
}}
</div>
<div class="col-md-9">
<h3>Selected Animals</h3>
<table class="table table-bordered">
<thead>
<tr>
<th>ID</th>
<th>Species</th>
<th>Name</th>
</tr>
</thead>
<tbody>
{{#each checkboxGroup.selectedItems as |animal|}}
<tr>
<td>{{animal.id}}</td>
<td>{{animal.species}}</td>
<td>{{animal.name}}</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
</div>
В нашем контроллере или маршрутизируемом компоненте мы вводим сервис и определяем действие для обработки отмеченного животного. Совокупность состояний сервиса затем передается в компонент, сохраняя его в максимально чистом виде. Хотя вы могли бы просто ввести сервис в компонент, но вышеописанный способ позволит внести в процесс ясность и отделить компонент от сервиса.
// animals/controller.js
import Ember from 'ember';
const { inject: { service }, Controller } = Ember;
const OPERATION_MAP = {
true: 'addObject',
false: 'removeObject'
};
export default Controller.extend({
checkboxGroup: service(),
// In the future, actions will be defined in the route and passed into the
// routable component as `attributes`.
actions: {
check(group, item, isChecked) {
return group[OPERATION_MAP[isChecked]](item);
}
}
});
Как упоминалось выше, сам по себе сервис простой. Позднее мы можем определить более сложное поведение, но здесь лежащее в основе сохранение для состояния — это просто массив JavaScript.
// checkbox-group/service.js
import Ember from 'ember';
const { Service } = Ember;
export default Service.extend({
init() {
this._super(...arguments);
this.selectedItems = [];
}
});
Так как в этом примере мы используем простое поведение, нет необходимости определять подкласс компонента. Действие check передается из маршрутизируемого компонента/контроллера, поэтому использование замкнутых действий в шаблоне компонента означает, что мы не должны приводить sendActions к void.
В нашем шаблоне компонента мы используем небольшие компонуемые хелперы. Эти хелперы, по сути, являются простыми функциями JavaScript. И так как они возвращают значения, мы можем использовать их как подвыражения Handlebars, где мы могли бы один раз определить вычислительное свойство.
Хелпер contains не входит в Ember изначально, но сама функция представляет собой одну строку кода. Есть ряд полезных дополнений, которые добавляют в приложение хелперы. Например, дополнение ember-truth-helpers я сама использую почти в каждом приложении.
{{! checkbox-group/template.hbs }}
{{#each group as |item|}}
<div class="checkbox">
<label for={{concat "item-" item.id}}>
{{one-way-input
id=(concat "item-" item.id)
class="checkbox"
type="checkbox"
checked=(contains selectedItems item)
update=(action this.attrs.check selectedItems item)
}} {{item.name}} <span class="label label-default">{{item.species}}</span>
</label>
</div>
{{/each}}
Как упоминалось в моей предыдущей статье, дополнение ember-one-way-input — простой способ уже сейчас использовать односторонние привязки.
Я надеюсь, что этот простой пример наглядно показывает, как вы можете создать удобное в сопровождении и эффективное приложение с помощью некоторых из моих любимых особенностей Ember: хелперов, замкнутых действий, компонентов и односторонних привязок.
Предостережение по поводу сервисов
Здесь все как в известном выражении: «Чем больше сила, тем больше и ответственность». Так как сервисы в Ember — это синглтоны, вас будет искушать желание делать по несколько сервисов и вводить их повсюду.
Если вы создаете сервис только для того, чтобы использовать его как глобальный объект, то в целом код будет «с душком», потому что зависимости станут неочевидными (ранее мы уже выяснили, что это плохо), и части приложения станут тесно связанными. Вместо этого раскрывайте данные и действия через интерфейсы, чтобы сохранить код разделенным и ясным. Помните принцип про силу и ответственность и используйте сервис только при крайней необходимости!
Когда же использовать сервисы?
Мне нравится ответ из Stack Overflow по поводу использования синглтонов. По сути, вам следует использовать их только в том случае, когда у вас может быть только один экземпляр во всем приложении. Например, корзина в интернет-магазине, лента активности или мгновенные сообщения могли бы стать отличными кандидатами для использования сервиса.
Перевод статьи — Ember.js?—?Goodbye MVC (Part 1)
Отдельное спасибо переводчикам и участникам сайта emjs.ru за предоставление статьи.
Комментарии (22)
Meliborn
08.01.2016 00:43+6стоит использовать Ember, который сразу предлагает производительность и низкий порог вхождения для разработчика
Чего-чего? Я недавно начал изучать Ember (сам пишу на рельсах) и это просто ад для новичка. С каких пор роуты рендерят темлейты и возвращают модели? Нафига вьюхи и где разница между ними, компонентами и темплейтами? А теперь еще контроллеры захотели убрать. Что за каша, зачем так усложнять? Я и так долго думаю в какие части этого эмбера засунуть нужный кусок кода и откуда что доступно. Простые вещи превращаются в часы планирования. Может быть со временем привыкнешь (как и ко всему) и не будешь думать куда что написать, но говорить что эмбер имеет легкий порок вхождения — это эребор.tenbits
08.01.2016 14:45> С каких пор роуты рендерят темлейты
Не могли бы вы подробнее остановиться на этом моменте? Это ведь довольно распространенная практика что за путём регистрируется лишь шаблон.falkon
08.01.2016 14:53По ссылкам ниже все подробно разобрано:
Отображение шаблона
Определение маршрутовtenbits
08.01.2016 15:01Спасибо за ссылки, но что плохого в том что «роуты рендерят темлейты»?
falkon
08.01.2016 15:19Это отличается от MVC, где за это отвечают контроллеры. Многим это не нравится, так как предлагается другой подход, причем контроллеры вообще хотят убрать из Ember заменив на компоненты.
Подробнее тут — Контроллерыtenbits
08.01.2016 15:28Ничего подобного, отличие лишь в том что входной точкой является шаблон. А шаблон может состоять лишь с одного компонента, например
. Далее вызывается контроллер «Users» и далее по старой, или вашими словами — привычной, схеме. Такое поведения на порядок лучше, чем прямой вызов контроллера «Users», так как шаблон разрешает создавать композицию из контроллеров и много прочих классных штук.<Users/>
falkon
08.01.2016 15:54так как шаблон разрешает создавать композицию из контроллеров
Это вы говорите о компонентах, для маршрута может быть только один соответствующий контроллер.tenbits
08.01.2016 17:48Компоненты можно тоже по принципу MVC строить, или одному из его вариаций.
> для маршрута может быть только один соответствующий контроллер
Это если только жестко привязываться к «Роут-Контроллер» архитектуре, но достаточно ввести между ними шаблон и уже можно комбинировать. И именно такой подход набирает популярность, так как он сочетает в себе и «Роут-Контроллер» вариант, и композицию.VasilioRuzanni
09.01.2016 12:06И именно такой подход набирает популярность, так как он сочетает в себе и «Роут-Контроллер» вариант, и композицию.
Потом уходит само (ненужное здесь, по большому счету) понятие «контроллера» и это просто называется компонентом :)tenbits
10.01.2016 23:06Ну как же, контроллер и компонент это разные вещи. Компонентом называют законченную единицу интерфейса/приложения — и он как раз состоит из контроллера, или шаблона, или только модели, также может другие ресурсы и компоненты включать в себя. Вообщем компоновать все это вещи можно как угодно. Здесь «компонент» был назван в контексте HMVC.
vintage
08.01.2016 15:09Дурная практика, на самом деле. Состояние в урле — это такое же состояние, как и состояние в локальном хранилище и состояние на сервере. В идеале работа со всеми этими состояниями должна быть единообразной. Более того, зачастую то какую страничку показать зависит сразу от нескольких типов состояний (конфиг пользователя с сервера, урл с переопределениями, локальное хранилище с теми данными, что не хотелось бы держать в урле).
tenbits
08.01.2016 15:31Вовсе нет. Шаблон в данном случае как middleware между роутом и контроллером, смотрите пример выше про Users. А всё вами описанное можно реализовать или «по-старинке» или же через композицию. Последнее намного удобнее.
vintage
08.01.2016 15:38Я не про шаблоны в данном случае, а про роуты. Состояние UI зависит не только и не столько от URL. Яркий пример — позиция проигрывания видео на вытрубе. Сохраняется в локальное хранилище, но может быть перегружена через URL.
tenbits
08.01.2016 15:46Вы меня запутали) Изначально обсуждалась тема, что плохого в том, что «роуты рендерят шаблоны». А если говорить лишь о URL, то разумеется не всегда возможно, или вернее сказать, не всегда целесообразно, хранить состояние приложения в путях.
vintage
08.01.2016 15:58+1Я к тому, что роуты — это довольно опциональные штуки и странно на них завязывать ядро фреймворка. Всё же это задача контроллера (пусть и в варианте сервиса или метода компонента) посмотреть в различные состояния и решить что и как рендерить.
vintage
08.01.2016 00:49+1В сочетании с паттерном DDAU и механизмом визуализации Glimmer разработчики Ember изначально имеют в своем распоряжении и производительность и эффективность.
Надеюсь они сотворили там чудо, ибо первая версия в два раза медленнее даже ангуляра: nin-jin.github.io/todomvc/benchmark
Кто бы написал ToDoMVC на Ember2?falkon
08.01.2016 12:32В Ember 2 производительность хорошо подтянули.
Свежее сравнение Ember 1, Ember 2, React и IDOM можно посмотреть в этом обзоре (En).vintage
08.01.2016 12:59Судя по графикам, еле-еле догнали React, который тоже не особо блещет производительностью. Впрочем, от модели виртуального дома лучшего наверно уже не добиться.
404
Не нужно, всё-таки, переводить дословно singleton и helper. Эти термины давно и успешно транслитерировались.
falkon
Поправил, спасибо.