Предлагаю вашему вниманию перевод статьи Ember.js — Goodbye MVC (Part 1).
На конференции 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)
Автор: falkon