8 распространенных ошибок разработчиков на Ember.js


Ember.js — комплексный фреймворк для создания сложных приложений на стороне клиента. Один из принципов фреймворка — "программирование по соглашениям". Кроме того, огромная часть процесса разработки характерна для большинства веб-приложений. Это отличное решение, которое позволяет справляться с повседневными трудностями. Но нужно время и участие со стороны сообщества, чтобы найти правильную абстракцию и охватить все случаи. Если рассуждать логически, то лучше потратить время, чтобы найти решение основной проблемы, и затем включить его во фреймворк, вместо того чтобы опускать руки и бросать всех разработчиков на произвол судьбы, когда им нужно это решение.

Ember.js постоянно совершенствуется, чтобы упростить разработку приложений. Но, как и в случае с любым продвинутым фреймворком, все еще есть подводные камни, с которыми могут столкнуться разработчики. В этой статье я предоставлю вам "карту", которая, надеюсь, позволит избежать распространенных подвохов. Приступим!!!

Ошибка #1: Разработчики ожидают, что hook модели запустится при передаче всех объектов контекста

Давайте предположим, что в нашем приложении есть такие маршруты:

Router.map(function() {
  this.route('band', { path: 'bands/:id' }, function() {
    this.route('songs');
  });
});

Маршрут band имеет динамический сегмент id. Когда приложение загружается с URL вроде /bands/24, 24 передается hook модели соответствующего маршрута, то есть band. Hook модели играет роль десериализатора сегмента для создания объекта (или массива объектов), которые затем можно использовать в шаблоне:

app/routes/band.js

export default Ember.Route.extend({
  model: function(params) {
    return this.store.find('band', params.id); // params.id is '24'
  }
});

Пока все хорошо. Но есть и другие варианты ввода маршрутов помимо загрузки приложения из навигационной панели браузера. Например, использовать хелпер link-to из шаблонов. Следующий фрагмент кода проходит по списку bands (групп) и создает соединение с их соответствующими маршрутами band:

{{#each bands as |band|}}
  {{link-to band.name "band" band}}
{{/each}}

Последний аргумент для link-to, band, это объект, который заполняет динамический сегмент для маршрута, и таким образом его id становится сегментом id для маршрута. Ловушка, в которую попадают многие разработчики, связана с тем, что hook модели в таком случае не вызывается, так как модель уже известна и передана. Это имеет смысл и должно освобождать от запроса к серверу. Но это, мягко говоря, нелегко понять. Самый оригинальный способ обойти проблему — это передать не сам объект, а его id:

{{#each bands as |band|}}
  {{link-to band.name "band" band.id}}
{{/each}}

Решение проблемы от Ember

В Ember в скором времени появятся маршрутизируемые компоненты (приблизительно в версии 2.1 или 2.2). Когда их внедрят в Ember, hook модели будет вызываться всегда, и неважно, как будет происходить переход к маршруту с динамическим сегментом. Обсуждение этого вопроса можно почитать здесь.

Ошибка #2: Разработчики забывают, что контроллеры, которыми управляет маршрут, являются синглтонами

В Ember.js маршруты устанавливают свойства на контроллеры, которые служат контекстом для соответствующего шаблона. Эти контроллеры — синглтоны, и поэтому любое установленное на них состояние сохраняется, даже когда контроллер больше не активен.

Это очень легко проглядеть, и я сам столкнулся с такой проблемой. У меня было приложение с музыкальным каталогом, который содержал группы и песни. Флажок songCreationStarted на контроллере songs указывал, что пользователь начинает создавать песню для определенной группы. Проблема была в том, что пользователь затем переключался на другую группу; значение songCreationStarted сохранялось. В итоге казалось, что наполовину законченная песня относилась к другой группе, что путало людей.

Чтобы решить проблему, необходимо вручную переустанавливать свойства контроллера, которые нам больше не нужны. Это можно сделать в hook setupController соответствующего маршрута, который вызывается на всех переходах после hook afterModel (как и указывает его название, он следует за hook модели).

app/routes/band.js

export default Ember.Route.extend({
  setupController: function(controller, model) {
    this._super(controller, model);
    controller.set('songCreationStarted', false);
  }
});

Решение проблемы от Ember

И снова приход маршрутизируемых компонентов решит эту проблему и полностью уберет контроллеры. Одним из преимуществ маршрутизируемых компонентов является то, что они имеют более постоянный жизненный цикл и всегда удаляются, когда осуществляется переход с их маршрутов. Когда они появятся, проблема выше потеряет свою актуальность.

Ошибка #3: Разработчики не вызывают реализацию по умолчанию в setupController

Маршруты в Ember имеют некоторое количество hooks жизненного цикла, чтобы определять специфическое для приложения поведение. Мы уже видели model, которая используется, чтобы извлечь данные для соответствующего шаблона, и setupController для установки контроллера, контекста шаблона.

setupController имеет удобную стандартную настройку, которая устанавливает модель из hook model как свойство model контроллера:

ember-routing/lib/system/route.js

setupController(controller, context, transition) {
  if (controller && (context !== undefined)) {
    set(controller, 'model', context);
  }
}

(context — имя, которое используется пакетом ember-routing, для чего выше я вызываю model)

Hook setupController можно переопределить, например, для переустановки состояния контроллера (как описано в ошибке #2). Но если забыть про вызов реализации предка, который я скопировал с Ember.Route выше, можно надолго зависнуть в недоумении, так как контроллер не получит набор параметров model. Поэтому всегда вызывайте this._super(controller, model):

export default Ember.Route.extend({
  setupController: function(controller, model) {
    this._super(controller, model);
    // put the custom setup here
  }
});

Решение проблемы от Ember

Как было указано выше, контроллеры, а вместе с ними и hook setupController, вскоре уберут. Поэтому такая проблема больше никого не побеспокоит. Но отсюда можно вынести хороший урок: нужно внимательнее относиться к реализациям предков. Функция init, которая определена в Ember.Object, мать всех объектов в Ember; с ней тоже надо быть аккуратнее.

Ошибка #4: Разработчики используют this.modelFor с неродительскими маршрутами

По мере обработки URL роутер Ember разрешает модель для каждого сегмента маршрута. Предположим, что в нашем приложении есть такие маршруты:

Router.map({
  this.route('bands', function() {
    this.route('band', { path: ':id' }, function() {
      this.route('songs');
    });
  });
});

Учитывая URL /bands/24/songs, порядок вызова будет таким: hook model - bands, bands.band, а затем bands.band.songs. API маршрута имеет довольно полезный метод modelFor. Его можно использовать в дочерних маршрутах, чтобы извлекать модель из какого-либо родительского маршрута, так как модель к этому моменту будет разрешена.

Например, следующий код — эффективный способ извлечь объект band в маршруте bands.band:

app/routes/bands/band.js

export default Ember.Route.extend({
  model: function(params) {
    var bands = this.modelFor('bands');
    return bands.filterBy('id', params.id);
  }
});

Но распространенной ошибкой в таком случае является использование имени маршрута в modelFor, которое не относится к предку маршрута. Немного изменим маршруты из примера выше:

Router.map({
  this.route('bands');
  this.route('band', { path: 'bands/:id' }, function() {
    this.route('songs');
  });
});

Наш метод для извлечения объявленной в URL band прервется, так как маршрут bands уже не будет предком, и таким образом его модель не будет разрешена.

app/routes/bands/band.js

export default Ember.Route.extend({
  model: function(params) {
    var bands = this.modelFor('bands'); // `bands` is undefined
    return bands.filterBy('id', params.id); // => error!
  }
});

Чтобы решить эту проблему, нужно использовать modelFor только для родительских маршрутов и применять другие способы для извлечения необходимых данных вместо modelFor. Например, извлекать из хранилища.

app/routes/bands/band.js

export default Ember.Route.extend({
  model: function(params) {
    return this.store.find('band', params.id);
  }
});

Ошибка #5: Разработчики неправильно понимают контекст, когда запускается действие компонента

Вложенные компоненты всегда были одной из самых сложных частей фреймворка Ember. C появлением блочных параметров в Ember 1.10 все стало проще, но во многих ситуациях пока еще проблематично сразу понять, какое действие компонента сработает при запуске из дочернего компонента.

Предположим, что у нас есть компонент band-list. Он содержит band-list-items, в котором мы можем отметить каждую группу как избранную.

app/templates/components/band-list.hbs

{{#each bands as |band|}}
  {{band-list-item band=band faveAction="setAsFavorite"}}
{{/each}}

Имя действия, которое должно вызываться, когда пользователь щелкает по кнопке, передается в компонент band-list-item и становится значением его свойства faveAction.

Теперь давайте взглянем на шаблон и определение компонента band-list-item.

app/templates/components/band-list-item.hbs

<div class="band-name">{{band.name}}</div>
<button class="fav-button" {{action "faveBand"}}>Fave this</button>

app/components/band-list-item.js

export default Ember.Component.extend({
  band: null,
  faveAction: '',

  actions: {
    faveBand: {
      this.sendAction('faveAction', this.get('band'));
    }
  }
});

Когда пользователь щелкает по кнопке "Fave this" (добавить в избранное), срабатывает действие faveBand. Оно запускает переданное faveAction компонента (в случае выше setAsFavorite) в родительском компоненте band-list.

Это сбивает с толку многих, так как разработчики ожидают, что действие запустится таким же образом, как это происходит с действиями из шаблонов, которыми управляют маршруты, то есть на контроллере (и затем распространится на активные маршруты). Что еще хуже, в журнале не появляется никаких сообщений об ошибке; родительский компонент просто "проглатывает" эту ошибку.

Здесь основное правило: действия запускаются по текущему контексту. В случае с шаблонами не компонентов, этим контекстом служит текущий контроллер. А в шаблонах компонента, им является родительский компонент (если он есть) или вновь текущий контроллер, если компонент не вложенный.

Поэтому в примере выше компонент band-list должен перезапустить действие, полученное из band-list-item, чтобы распространить его на контроллер или маршрут.

app/components/band-list.js

export default Ember.Component.extend({
  bands: [],
  favoriteAction: 'setFavoriteBand',

  actions: {
    setAsFavorite: function(band) {
      this.sendAction('favoriteAction', band);
    }
  }
});

Если band-list был определен в шаблоне bands, действие setFavoriteBand обрабатывалось бы в контроллере bands или маршруте bands (или одном из родительских маршрутов).

Решение проблемы от Ember

Несложно представить, что все это запутает еще сильнее, если появится больше уровней вложенности (например, если расположить компонент fav-button внутри band-list-item). И придется пробиваться изнутри через несколько слоев, чтобы вытащить ваше сообщение, определяя необходимые имена на каждом уровне (setAsFavorite, favoriteAction, faveAction).

Все станет проще благодаря "Улучшенным действиям", которые уже доступны в ветке master и, вероятно, будут включены в версию 1.13.

Пример выше тогда будет упрощен так:

app/templates/components/band-list.hbs

{{#each bands as |band|}}
  {{band-list-item band=band setFavBand=(action "setFavoriteBand")}}
{{/each}}

app/templates/components/band-list-item.hbs

<div class="band-name">{{band.name}}</div>
<button class="fav-button" {{action "setFavBand" band}}>Fave this</button>

Ошибка #6: Разработчики используют свойства массива как зависимые ключи

Вычисляемые свойства Ember зависят от других свойств, и эту зависимость разработчик должен подробно определять. Скажем, у нас есть свойство isAdmin, которое должно быть true, только если одна из roles (ролей) будет admin. Вот так это должно быть записано:

isAdmin: function() {
  return this.get('roles').contains('admin');
}.property('roles')

Учитывая код выше, значение isAdmin признается недействительным, если меняется сам объект массива roles, но не в том случае, если элементы добавляются или удаляются из существующего массива. Есть специальный синтаксис, который определяет, что добавления и удаления должны также запускать повторное вычисление:

isAdmin: function() {
  return this.get('roles').contains('admin');
}.property('roles.[]')

Ошибка #7: Разработчики не используют ориентированные на observer (наблюдателя) методы

Давайте расширим (теперь уже исправленный) пример из Ошибки #6 и создадим в приложении класс User.

var User = Ember.Object.extend({
  initRoles: function() {
    var roles = this.get('roles');
    if (!roles) {
      this.set('roles', []);
    }
  }.on('init'),

  isAdmin: function() {
    return this.get('roles').contains('admin');
  }.property('roles.[]')
});

Когда мы добавляем роль admin к такому User, получаем сюрприз:

var user = User.create();
user.get('isAdmin'); // => false
user.get('roles').push('admin');
user.get('isAdmin'); // => false ?

Проблема в том, что наблюдатели не запустятся (и таким образом вычисляемые свойства не обновятся), если используются стандартные методы Javascript. Проблема должна исчезнуть, если в браузерах улучшат глобальную адаптацию Object.observe. Но до тех пор, нам придется использовать ряд методов, которые предлагает Ember. В текущем случае pushObject служит ориентированным на наблюдателя эквивалентом push:

user.get('roles').pushObject('admin');
user.get('isAdmin'); // => true, finally!

Ошибка #8: Разработчики передают изменения свойствам в компонентах

Представьте, что у нас есть компонент star-rating, который отображает рейтинг элементов и позволяет его настраивать. Рейтинг может относиться к песне, книге или навыку дриблинга у игрока в футбол.

Вы могли бы использовать его в шаблоне таким образом:

{{#each songs as |song|}}
  {{star-rating item=song rating=song.rating}}
{{/each}}

Далее, предположим, что компонент показывает звезды: одну полную звезду за каждый балл, затем пустые звезды до максимума. Когда по звезде щелкают, на контроллере запускается действие set. То есть подразумевается, что пользователь хочет обновить рейтинг. Для этого мы могли бы написать следующий код:

app/components/star-rating.js

export default Ember.Component.extend({
  item: null,
  rating: 0,
  (...)
  actions: {
    set: function(newRating) {
      var item = this.get('item');
      item.set('rating', newRating);
      return item.save();
    }
  }
});

Мы добились своего, но возникает ряд проблем. Предполагается, что переданный элемент имеет свойство rating, и поэтому мы не можем использовать этот компонент, чтобы управлять оценкой дриблинга Лео Месси (где это свойство должно называться score).

И еще меняется рейтинг элемента в компоненте. Это приводит к сценариям, где трудно понять, почему конкретное свойство меняется. Представьте, что у нас есть еще один компонент в том же шаблоне, где также используется рейтинг, например, для вычисления средней оценки игрока.

Слоган для упрощения этого запутанного сценария — "Данные вниз, действия вверх" (DDAU). Данные должные передаваться вниз (от маршрута к контроллеру и к компонентам), а компоненты должны использовать действия, чтобы извещать их контекст об изменениях в этих данных. Как же применить DDAU в этом случае?

Давайте добавим имя действия, которое необходимо отправить для обновления рейтинга:

{{#each songs as |song|}}
  {{star-rating item=song rating=song.rating setAction="updateRating"}}
{{/each}}

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

app/components/star-rating.js

export default Ember.Component.extend({
  item: null,
  rating: 0,
  (...)
  actions: {
    set: function(newRating) {
      var item = this.get('item');
      this.sendAction('setAction', {
        item: this.get('item'),
        rating: newRating
      });
    }
  }
});

Наконец, действие обрабатывается "восходящим потоком", контроллером или маршрутом. И вот где рейтинг элемента обновляется:

app/routes/player.js

export default Ember.Route.extend({
  actions: {
    updateRating: function(params) {
      var skill = params.item,
          rating = params.rating;

      skill.set('score', rating);
      return skill.save();
    }
  }
});

Когда это происходит, изменение распространяется ниже через привязку, переданную компоненту star-rating, и в результате количество полных звезд меняется.

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

Мы могли бы применить тот же компонент для навыков в футболе:

{{#each player.skills as |skill|}}
  {{star-rating item=skill rating=skill.score setAction="updateSkill"}}
{{/each}}

Заключение

Важно отметить, что некоторые (большинство?) ошибок, которые совершал я или другие разработчики, включая те, что я здесь описал, постепенно исчезают или уже упрощены в версиях 2.x Ember.js.

Те, что остались, я здесь и указал. Поэтому, если вы разрабатываете приложения в Ember 2.x, то просто не имеете права совершать еще какие-либо ошибки!

Перевод статьи - Ember.js and The 8 Most Common Mistakes that Developers Make


Комментарии (0)

    Выделите опечатку и нажмите Ctrl + Enter, чтобы отправить сообщение об ошибке.