Pull to refresh

Ember.js — прощай MVC (часть 1)

Reading time6 min
Views27K
Предлагаю вашему вниманию перевод статьи 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 раскрывается по мере роста и усложнения приложения. Каскадное обновление и неочевидные зависимости ведут к неразберихе и путанице. Обновление одного объекта приводит к изменению другого, что, в свою очередь, запускает следующие процессы и в конечном счете превращает поддержку приложения в настоящий «кошмар».

imageEmber 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 за предоставление статьи.
Tags:
Hubs:
Total votes 15: ↑14 and ↓1+13
Comments22

Articles