8 самых распространенных ошибок, которые допускают разработчики Ember.js

Опубликовано: 2022-03-11

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

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

Распространенная ошибка № 1: Ожидание срабатывания хука модели при передаче всех объектов контекста

Предположим, что в нашем приложении есть следующие маршруты:

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

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

 // 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 из шаблонов. Следующий фрагмент просматривает список групп и создает ссылку на соответствующие маршруты band :

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

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

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

Ember.js

План смягчения последствий Эмбер

Маршрутизируемые компоненты скоро появятся в Ember, вероятно, в версии 2.1 или 2.2. Когда они приземляются, хук модели всегда будет вызываться, независимо от того, как вы переходите на маршрут с динамическим сегментом. Прочтите соответствующий RFC здесь.

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

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

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

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

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

План смягчения последствий Эмбер

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

Распространенная ошибка № 3: Не вызывать реализацию по умолчанию в setupController

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

Этот последний, setupController , имеет разумное значение по умолчанию, которое назначает модель из хука model в качестве свойства model контроллера:

 // ember-routing/lib/system/route.js setupController(controller, context, transition) { if (controller && (context !== undefined)) { set(controller, 'model', context); } }

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

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 } });

План смягчения последствий Эмбер

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

Распространенная ошибка № 4: использование this.modelFor с неродительскими маршрутами

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

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

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

Например, следующий код является допустимым способом получения объекта полосы в маршруте 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-адресе, не работает, так как маршрут 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. С введением блочных параметров в 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')); } } });

Когда пользователь нажимает кнопку «Добавить в избранное», запускается действие 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 (или одном из его родительских маршрутов).

План смягчения последствий Эмбер

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

Это упрощается за счет «RFC для улучшенных действий», который уже доступен в основной ветке и, вероятно, будет включен в 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 тогда и только тогда, когда одна из ролей — admin . Вот как это можно написать:

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

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

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

Распространенная ошибка № 7: неиспользование удобных для наблюдателя методов

Давайте расширим (теперь исправленный) пример из Общей ошибки № 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, у вас больше не будет оправданий для ошибок! Если вам нужна эта статья в формате pdf, зайдите в мой блог и нажмите на ссылку внизу поста.

Обо мне

Я пришел в мир интерфейсов с Ember.js два года назад, и я здесь, чтобы остаться. Я так увлекся Ember, что начал интенсивно вести блог как в гостевых постах, так и в своем собственном блоге, а также выступать на конференциях. Я даже написал книгу « Рок-н-ролл с Ember.js » для всех, кто хочет изучить Ember. Вы можете скачать образец главы здесь.