Руководство по созданию вашего первого приложения Ember.js

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

Поскольку современные веб-приложения все больше и больше работают на стороне клиента (сам факт, что мы теперь называем их «веб-приложениями», а не «веб-сайтами», говорит о многом), интерес к инфраструктурам на стороне клиента растет. . В этой области много игроков, но среди приложений с большим количеством функций и множеством движущихся частей особенно выделяются два из них: Angular.js и Ember.js.

Мы уже опубликовали подробное [руководство по Angular.js][https://www.toptal.com/angular-js/a-step-by-step-guide-to-your-first-angularjs-app], поэтому мы В этом посте мы сосредоточимся на Ember.js, в котором мы создадим простое приложение Ember для каталогизации вашей музыкальной коллекции. Вы познакомитесь с основными строительными блоками фреймворка и получите представление о принципах его проектирования. Если вы хотите увидеть исходный код во время чтения, он доступен в виде рок-н-ролла на Github.

Что будем строить?

Вот как наше приложение Rock & Roll будет выглядеть в финальной версии:

Финальная версия приложения с ember.js

Слева вы увидите, что у нас есть список исполнителей, а справа список песен выбранного исполнителя (вы также можете видеть, что у меня хороший музыкальный вкус, но я отвлекся). Новых исполнителей и песни можно добавить, просто введя текст в текстовое поле и нажав соседнюю кнопку. Звезды рядом с каждой песней служат для ее оценки в стиле iTunes.

Мы могли бы разбить элементарную функциональность приложения на следующие шаги:

  1. Нажатие «Добавить» добавляет в список нового исполнителя с именем, указанным в поле «Новый исполнитель» (то же самое касается песен для данного исполнителя).
  2. Очистка поля «Новый исполнитель» отключает кнопку «Добавить» (то же самое касается песен для данного исполнителя).
  3. Если щелкнуть имя исполнителя, справа отобразятся его песни.
  4. Щелчок по звездам оценивает данную песню.

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

Маршруты: ключ к приложению Ember.js

Одной из отличительных особенностей Ember является то, что он уделяет большое внимание URL-адресам. Во многих других фреймворках отдельные URL-адреса для отдельных экранов либо отсутствуют, либо добавляются с запозданием. В Ember маршрутизатор — компонент, который управляет URL-адресами и переходами между ними — является центральной частью, которая координирует работу между строительными блоками. Следовательно, это также ключ к пониманию внутренней работы приложений Ember.

Вот маршруты для нашего приложения:

 App.Router.map(function() { this.resource('artists', function() { this.route('songs', { path: ':slug' }); }); });

Мы определяем маршрут ресурса, artists и маршрут songs , вложенный в него. Это определение даст нам следующие маршруты:

Маршруты

Я использовал отличный плагин Ember Inspector (он существует как для Chrome, так и для Firefox), чтобы показать вам сгенерированные маршруты в легко читаемом виде. Вот основные правила для маршрутов Ember, которые вы можете проверить для нашего конкретного случая с помощью приведенной выше таблицы:

  1. Существует неявный маршрут application .

    Это активируется для всех запросов (переходов).

  2. Существует неявный index маршрут.

    Это вводится, когда пользователь переходит в корень приложения.

  3. Каждый ресурсный маршрут создает маршрут с тем же именем и неявно создает индексный маршрут под ним.

    Этот индексный маршрут активируется, когда пользователь переходит к маршруту. В нашем случае artists.index срабатывает, когда пользователь переходит к /artists .

  4. Простой (не ресурсный) вложенный маршрут будет иметь имя родительского маршрута в качестве префикса.

    Маршрут, который мы определили как this.route('songs', ...) , будет иметь в качестве имени artists.songs . Он срабатывает, когда пользователь переходит к /artists/pearl-jam или /artists/radiohead .

  5. Если путь не указан, предполагается, что он равен имени маршрута.

  6. Если путь содержит : , он считается динамическим сегментом .

    Присвоенное ему имя (в нашем случае slug ) будет соответствовать значению в соответствующем сегменте URL-адреса. Сегмент slug выше будет иметь значение pearl-jam , radiohead или любое другое значение, которое было извлечено из URL-адреса.

Показать список исполнителей

В качестве первого шага мы создадим экран, отображающий список исполнителей слева. Этот экран должен отображаться для пользователей, когда они переходят к /artists/ :

Художники

Чтобы понять, как отображается этот экран, пришло время представить еще один всеобъемлющий принцип проектирования Ember: соглашение важнее конфигурации . В приведенном выше разделе мы видели, что /artists активирует маршрут artists . По соглашению имя этого объекта маршрута — ArtistsRoute . Этот объект маршрута отвечает за получение данных для отображения приложением. Это происходит в хуке модели маршрута:

 App.ArtistsRoute = Ember.Route.extend({ model: function() { var artistObjects = []; Ember.$.getJSON('http://localhost:9393/artists', function(artists) { artists.forEach(function(data) { artistObjects.pushObject(App.Artist.createRecord(data)); }); }); return artistObjects; } });

В этом фрагменте данные извлекаются с помощью вызова XHR из серверной части и — после преобразования в объект модели — помещаются в массив, который мы можем впоследствии отобразить. Однако обязанности маршрута не распространяются на обеспечение логики отображения, за которую отвечает контроллер. Давайте взглянем.

Хммм, на самом деле нам не нужно определять контроллер в этот момент! Ember достаточно умен, чтобы генерировать контроллер , когда это необходимо, и устанавливать атрибут M.odel контроллера на возвращаемое значение самого хука модели, а именно на список художников. (Опять же, это результат парадигмы «соглашение важнее конфигурации».) Мы можем перейти на один уровень ниже и создать шаблон для отображения списка:

 <script type="text/x-handlebars" data-template-name="artists"> <div class="col-md-4"> <div class="list-group"> {{#each model}} {{#link-to "artists.songs" this class="list-group-item artist-link"}} {{name}} <span class="pointer glyphicon glyphicon-chevron-right"></span> {{/link-to}} {{/each}} </div> </div> <div class="col-md-8"> <div class="list-group"> {{outlet}} </div> </div> </script>

Если это выглядит знакомо, то это потому, что Ember.js использует шаблоны Handlebars, которые имеют очень простой синтаксис и помощники, но не позволяют использовать нетривиальную логику (например, операторы OR или AND в условном выражении).

В приведенном выше шаблоне мы перебираем модель (настроенную ранее путем маршрута к массиву, содержащему всех исполнителей) и для каждого элемента в нем мы отображаем ссылку, которая ведет нас к маршруту artists.songs для этого исполнителя. Ссылка содержит имя исполнителя. #each в Handlebars изменяет область внутри себя на текущий элемент, поэтому {{name}} всегда будет ссылаться на имя исполнителя, который в данный момент находится в стадии итерации.

Вложенные маршруты для вложенных представлений

Еще один интересный момент в приведенном выше фрагменте: {{outlet}} , который указывает слоты в шаблоне, где контент может отображаться. При вложении маршрутов сначала отображается шаблон для внешнего ресурсного маршрута, за которым следует внутренний маршрут, который отображает содержимое своего шаблона в {{outlet}} , определяемый внешним маршрутом. Это именно то, что происходит здесь.

По соглашению все маршруты отображают свое содержимое в одноименном шаблоне. Выше атрибут data-template-name вышеуказанного шаблона — artists , что означает, что он будет отображаться для внешнего маршрута, artists . Он указывает выход для содержимого правой панели, в который внутренний маршрут, artists.index отображает свое содержимое:

 <script type="text/x-handlebars" data-template-name="artists/index"> <div class="list-group-item empty-list"> <div class="empty-message"> Select an artist. </div> </div> </script>

Таким образом, один маршрут ( artists ) отображает свое содержимое на левой боковой панели, его моделью является список художников. Другой способ: artists.index отображает собственный контент в слоте, предоставленном artists Artists. Он может получить некоторые данные, которые послужат его моделью, но в этом случае все, что мы хотим отобразить, — это статический текст, поэтому нам это не нужно.

Связанный: 8 основных вопросов для интервью Ember.js

Создать исполнителя

Часть 1. Привязка данных

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

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

 <script type="text/x-handlebars" data-template-name="artists"> <div class="col-md-4"> <div class="list-group"> <div class="list-group-item"> {{input type="text" class="new-artist" placeholder="New Artist" value=newName}} <button class="btn btn-primary btn-sm new-artist-button" {{action "createArtist"}} {{bind-attr disabled=disabled}}>Add</button> </div> < this is where the list of artists is rendered > ... </script>

Мы используем хелпер Ember input с типом text для рендеринга простого ввода текста. В нем мы привязываем значение ввода текста к свойству newName контроллера, который резервирует этот шаблон, ArtistsController . Как следствие, когда свойство value ввода изменяется (другими словами, когда пользователь вводит в него текст), свойство newName на контроллере будет синхронизироваться.

Мы также сообщаем, что действие createArtist должно запускаться при нажатии кнопки. Наконец, мы привязываем свойство disabled кнопки к свойству disabled контроллера. Так как же выглядит контроллер?

 App.ArtistsController = Ember.ArrayController.extend({ newName: '', disabled: function() { return Ember.isEmpty(this.get('newName')); }.property('newName') });

newName в начале установлено пустым, что означает, что ввод текста будет пустым. (Помните, что я говорил о привязках? Попробуйте изменить newName и посмотрите, как оно отобразится в виде текста в поле ввода.)

disabled реализован таким образом, что когда в поле ввода нет текста, он возвращает значение true , и, таким образом, кнопка будет отключена. Вызов .property в конце делает это «вычисляемым свойством», еще одним восхитительным кусочком торта Ember.

Вычисляемые свойства — это свойства, зависящие от других свойств, которые сами по себе могут быть «обычными» или вычисляемыми. Ember кэширует их значения до тех пор, пока не изменится одно из зависимых свойств. Затем он пересчитывает значение вычисляемого свойства и снова кэширует его.

Вот визуальное представление вышеописанного процесса. Подводя итог: когда пользователь вводит имя исполнителя, свойство newName обновляется, за ним следует свойство disabled и, наконец, имя исполнителя добавляется в список.

Объезд: единственный источник правды

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

Чем больше система, тем больше преимуществ мы получаем от принципа «единого источника правды». Это делает наш код чистым и надежным, а определения свойств — более декларативными.

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

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

Часть 2. Обработчики действий

Вернемся к тому, как создается действие createArtist после его запуска (после нажатия кнопки):

 App.ArtistsRoute = Ember.Route.extend({ ... actions: { createArtist: function() { var name = this.get('controller').get('newName'); Ember.$.ajax('http://localhost:9393/artists', { type: 'POST', dataType: 'json', data: { name: name }, context: this, success: function(data) { var artist = App.Artist.createRecord(data); this.modelFor('artists').pushObject(artist); this.get('controller').set('newName', ''); this.transitionTo('artists.songs', artist); }, error: function() { alert('Failed to save artist'); } }); } } });

Обработчики действий должны быть заключены в объект actions и могут быть определены на маршруте, контроллере или представлении. Я решил определить его на маршруте здесь, потому что результат действия не ограничен контроллером, а скорее «глобальный».

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

  1. Добавьте нового исполнителя в модель шаблона (всех исполнителей), чтобы она была повторно визуализирована, а новый исполнитель отображался последним элементом списка.
  2. Очистите поле ввода с помощью привязки newName , избавив нас от необходимости напрямую манипулировать DOM.
  3. Переход к новому маршруту ( artists.songs ), передавая только что созданного исполнителя в качестве модели для этого маршрута. transitionTo — это способ внутреннего перемещения между маршрутами. (Помощник link-to служит для этого с помощью действий пользователя.)

Показать песни для исполнителя

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

Активный маршрут здесь — artists.songs , поэтому контроллер и шаблон будут ArtistsSongsController и Artists artists/songs соответственно. Мы уже видели, как шаблон отображается в выходном файле, предоставляемом шаблоном artists , поэтому мы можем сосредоточиться только на имеющемся шаблоне:

 <script type="text/x-handlebars" data-template-name="artists/songs"> (...) {{#each songs}} <div class="list-group-item"> {{title}} {{view App.StarRating maxRating=5}} </div> {{/each}} </script>

Обратите внимание, что я удалил код для создания новой песни, поскольку он будет точно таким же, как и для создания нового исполнителя.

Свойство songs настраивается во всех объектах исполнителя на основе данных, возвращаемых сервером. Точный механизм, с помощью которого это делается, мало интересует нынешнюю дискуссию. Пока нам достаточно знать, что у каждой песни есть название и рейтинг.

Название отображается непосредственно в шаблоне, а рейтинг представлен звездами в представлении StarRating . Давайте посмотрим, что сейчас.

Виджет звездного рейтинга

Рейтинг песни колеблется от 1 до 5 и показывается пользователю через представление App.StarRating . Представления имеют доступ к своему контексту (в данном случае к песне) и своему контроллеру. Это означает, что они могут читать и изменять его свойства. Это контрастирует с другим строительным блоком Ember, компонентами, которые являются изолированными, многократно используемыми элементами управления с доступом только к тому, что им было передано. (В этом примере мы могли бы также использовать компонент звездного рейтинга.)

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

 App.StarRating = Ember.View.extend({ classNames: ['rating-panel'], templateName: 'star-rating', rating: Ember.computed.alias('context.rating'), fullStars: Ember.computed.alias('rating'), numStars: Ember.computed.alias('maxRating'), stars: function() { var ratings = []; var fullStars = this.starRange(1, this.get('fullStars'), 'full'); var emptyStars = this.starRange(this.get('fullStars') + 1, this.get('numStars'), 'empty'); Array.prototype.push.apply(ratings, fullStars); Array.prototype.push.apply(ratings, emptyStars); return ratings; }.property('fullStars', 'numStars'), starRange: function(start, end, type) { var starsData = []; for (i = start; i <= end; i++) { starsData.push({ rating: i, full: type === 'full' }); }; return starsData; }, (...) });

rating , fullStars и numStars -- это вычисляемые свойства , которые мы обсуждали ранее с disabled свойством ArtistsController . Выше я использовал так называемый макрос вычисляемых свойств, около дюжины которых определены в Ember. Они делают типичные вычисляемые свойства более краткими и менее подверженными ошибкам (для записи). Я установил rating как рейтинг контекста (и, следовательно, песни), в то время как я определил свойства fullStars и numStars , чтобы они лучше читались в контексте виджета звездного рейтинга.

Метод stars является главной достопримечательностью. Он возвращает массив данных для звезд, в котором каждый элемент содержит свойство rating (от 1 до 5) и флаг ( full ), указывающий, заполнена ли звезда. Это делает чрезвычайно простым просмотр их в шаблоне:

 <script type="text/x-handlebars" data-template-name="star-rating"> {{#each view.stars}} <span {{bind-attr data-rating=rating}} {{bind-attr class=":star-rating :glyphicon full:glyphicon-star:glyphicon-star-empty"}} {{action "setRating" target=view}}> </span> {{/each}} </script>

Этот фрагмент содержит несколько замечаний:

  1. Во-первых, each помощник указывает, что он использует свойство представления (в отличие от свойства контроллера), добавляя префикс view к имени свойства.
  2. Во-вторых, атрибут class тега span имеет смешанные динамические и статические классы. Все, что имеет префикс : , становится статическим классом, в то время как full:glyphicon-star:glyphicon-star-empty похожа на тернарный оператор в JavaScript: если полное свойство истинно, должен быть назначен первый класс; если нет, то второй.
  3. Наконец, когда тег нажат, действие setRating должно быть запущено, но Ember будет искать его в представлении, а не в маршруте или контроллере, как в случае создания нового исполнителя.

Таким образом, действие определяется в представлении:

 App.StarRating = Ember.View.extend({ (...) actions: { setRating: function() { var newRating = $(event.target).data('rating'); this.set('rating', newRating); } } });

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

Завершение всего этого

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

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

Красиво, не так ли?

Дальнейшее чтение (и просмотр)

В Ember гораздо больше, чем я мог бы вместить в один только этот пост. Если вы хотите посмотреть серию скринкастов о том, как я создал несколько более совершенную версию вышеупомянутого приложения, и/или узнать больше об Ember, вы можете подписаться на мой список рассылки, чтобы еженедельно получать статьи или советы.

Надеюсь, я пробудил у вас желание узнать больше об Ember.js и что вы пойдете дальше примера приложения, которое я использовал в этом посте. Продолжая изучать Ember.js, обязательно прочитайте нашу статью о Ember Data, чтобы узнать, как использовать библиотеку ember-data. Получайте удовольствие от строительства!

Связанный: Ember.js и 8 наиболее распространенных ошибок, которые допускают разработчики