Продолжаем публиковать перевод туториала из официального руководства Ember.js. Туториал состоит из двух частей и это вторая половина первой части туториала. Напоминаем, что первую половину вы можете прочитать по этой ссылке
Список тем, которые покрывает туториал внушает:
- Использование Ember CLI
- Навигация по структуре файлов и папок приложения Ember
- Создание и связь между страницами
- Шаблоны (templates) и компоненты
- Автоматизированное тестирование
- Работа с данными сервера
- Динамические сегменты в маршрутах
- Сервисы в Ember
- Библиотека Ember Data
- Адаптеры и сериализаторы
- Паттерн компонента-провайдера
Садитесь поудобнее, открывайте терминалы, находите проект на своем компьютере и давайте двигаться дальше. И помните, что если у вас возникнут трудности, всегда можно попросить помощи в Discord канале сообщества (на русском канал #lang-russian), а также в русскоязычном телеграмм канале ember_js
Подробнее о компонентах
Пора наконец-то поработать над списком аренды:
Составляя этот список арендуемой недвижимости, вы узнаете о:
- Генерации компонентов
- Организации кода с использованием компонентов пространства имен
- Пересылке атрибутов HTML с
...attributes
- Определении подходящего количества тестового покрытия
Генерация компонентов
Начнем с создания компонента <Rental>
. На этот раз мы будем использовать генератор компонентов для создания шаблона и тестового файла для нас:
$ ember generate component rental
installing component
create app/components/rental.hbs
skip app/components/rental.js
tip to add a class, run `ember generate component-class rental`
installing component-test
create tests/integration/components/rental-test.js
Генератор создал для нас два новых файла: шаблон компонента в app/components/rental.hbs
и тестовый файл компонента в tests/integration/components/rental-test.js
.
Мы начнем с редактирования шаблона. Давайте пока захардкодим детали для одного арендуемого объекта, а потом заменим его реальными данными с сервера.
Затем мы напишем тест, чтобы убедиться, что все детали присутствуют. Мы заменим шаблонный тест, сгенерированный для нас, нашими собственными командами, как мы делали это ранее для компонента <Jumbo>
:
Тесты должны пройти.
Наконец, давайте добавим новый компонент в шаблон индекса, чтобы заполнить страницу.
При этом мы должны увидеть компонент <Rental>
показывающий наш Grand Old Mansion три раза на странице:
Все выглядит довольно неплохо для небольшой работы!
Организация кода с использованием пространств имен
Далее добавим изображение для арендуемой недвижимости. Мы будем использовать генератор компонентов для этого снова:
$ ember generate component rental/image
installing component
create app/components/rental/image.hbs
skip app/components/rental/image.js
tip to add a class, run `ember generate component-class rental/image`
installing component-test
create tests/integration/components/rental/image-test.js
На этот раз у нас был /
в названии компонента. Это привело к созданию компонента в app/components/rental/image.hbs
, который может быть вызван как <Rental::Image>
.
Подобные компоненты известны как компоненты из пространства имен. Пространство имен позволяет нам организовывать наши компоненты по папкам в соответствии с их назначением. Это совершенно необязательно, но удобно, особенно, когда вы разрабатываете крупное приложение в коллективе.
Пересылка атрибутов HTML с ...attributes
Давайте отредактируем шаблон компонента:
Вместо того, чтобы жестко задавать конкретные значения для атрибутов src
и alt
в <img>
, мы выбрали ключевое слово ...attributes
, которое также иногда называют «splattributes». Это позволяет передавать произвольные атрибуты HTML при вызове этого компонента, например так:
Мы указали здесь атрибут HTML src
и alt
, который будет передан компоненту и присоединен к элементу, где ...attributes
применяются в шаблоне компонента. Вы можете думать, что это похоже на {{yield}}
, но только для атрибутов HTML, а не для отображаемого содержимого. Фактически, мы уже использовали эту функцию ранее, когда передавали атрибут class
в <LinkTo>
.
Таким образом, наш компонент <Rental::Image>
не связан с какой-либо конкретной арендуемой собственностью на сайте. Конечно, у нас все также жестко закодировано, но мы с этим скоро разберемся. Пока же ограничим весь хардкод компонентом , чтобы было легче очистить его, когда мы перейдем к извлечению реальных данных.
В общем, это хорошая идея добавить ...attributes
к корневому элементу в вашем компоненте. Это обеспечит максимальную гибкость, так как инициатору может потребоваться передать классы для стилизации или атрибуты ARIA для улучшения доступности.
А теперь давайте напишем тест для нашего нового компонента!
Определение подходящего количества тестового покрытия
Наконец, мы также должны обновить тесты для компонента <Rental>
чтобы убедиться, что мы успешно вызвали <Rental::Image>
.
Поскольку мы уже написали тесты, относящиеся к <Rental::Image>
, здесь мы можем опустить детали и свести проверку к минимуму. Таким образом, нам также не придется обновлять тест <Rental>
всякий раз, когда мы вносим изменения в <Rental::Image>
.
Интерактивные компоненты
В этой главе вы добавите интерактивность на страницу, которая позволит пользователю кликать на изображение, чтобы увеличить или уменьшить его:
При этом вы узнаете о:
- Добавлении поведения к компонентам с классами
- Доступе к состояниям экземпляра из шаблонов
- Управлении состоянием с отслеживаемыми свойствами
- Использовании условных синтаксисов в шаблонах
- Реагировании на взаимодействие пользователя с действиями
- Как вызвать модификаторы элемента
- Тестировании взаимодействия с пользователем
Добавление поведения к компонентам с классами
Пока что все компоненты, которые мы написали, являются чисто презентационными — это просто фрагменты разметки многократного использования. Это конечно, прекрасно, но в Ember компоненты могут сделать намного больше!
Иногда вы хотите связать некоторое поведение с вашими компонентами, чтобы они могли делать более интересные вещи. Например, <LinkTo>
может реагировать на клики, изменяя URL-адрес и переходя на другую страницу.
Здесь мы собираемся сделать что-то похожее! Мы собираемся реализовать функциональность «View Larger»
и «View Smaller»
, которая позволит нашим пользователям щелкнуть на изображение дома, просмотреть увеличенную версию, и снова щелкнуть по нему, чтобы вернуться к уменьшенной версии.
Другими словами, нам нужен способ переключения изображения между одним из двух состояний. Чтобы сделать это, нам нужен способ для компонента хранить два возможных состояния и знать, в каком состоянии он находится в данный момент.
Ember дополнительно позволяет нам связать код JavaScript с компонентом именно для этой цели. Мы можем добавить файл JavaScript для нашего компонента <Rental::Image>
, запустив генератор компонента:
$ ember generate component-class rental/image
installing component-class
create app/components/rental/image.js
Эта команда сгенерировала файл JavaScript с тем же именем, что и шаблон нашего компонента в app/components/rentals/image.js
. Он содержит класс JavaScript, унаследованный от @glimmer/component
.
Зои поясняет ...
@glimmer/component
или компонент Glimmer — это один из нескольких классов компонентов, доступных для использования. Они являются отличной отправной точкой, когда вы хотите добавить поведение (behavior) к своим компонентам. В этом уроке мы будем использовать исключительно компоненты Glimmer.
В общем, компоненты Glimmer следует использовать всегда, когда это возможно. Тем не менее, вы также можете увидеть@ember/components
(классические компоненты), используемые в старых приложениях. Вы можете отличить их друг от друга, посмотрев на путь их импорта (что полезно при поиске соответствующей документации, поскольку у них разные и несовместимые API).
Ember создает экземпляр класса всякий раз, когда вызывается наш компонент. Мы можем использовать этот экземпляр для хранения нашего состояния:
Здесь, в конструкторе компонента, мы инициализировали переменную экземпляра this.isLarge
со значением false
, так как это состояние по умолчанию, которое мы хотим для нашего компонента.
Доступ к состояниям экземпляра из шаблонов
Давайте обновим наш шаблон, чтобы использовать это состояние, которое мы только что добавили:
В шаблоне у нас есть доступ к переменным экземпляра компонента. Условный синтаксис {{#if ...}}...{{else}}...{{/if}}
позволяет отображать различное содержимое в зависимости от условия (в данном случае это значение переменной экземпляра this.isLarge
). Комбинируя эти две функции, мы можем соответственно визуализировать как маленькую, так и большую версию изображения.
Мы можем это проверить временно изменив начальное значение в нашем файле JavaScript. Если мы изменим app/components/rental/image.js
для инициализации this.isLarge = true
; в конструкторе мы должны увидеть большую версию изображения свойства в браузере. Здорово!
После того как мы проверили, можно изменить this.isLarge
обратно на false
.
Поскольку этот шаблон инициализации переменных экземпляра в конструкторе довольно распространен, для него существует гораздо более краткий синтаксис:
Такая же функциональность, но намного короче!
Конечно, наши пользователи не могут редактировать наш исходный код, поэтому нам нужен способ для переключения размера изображения из браузера. В частности, мы хотим переключать значение this.isLarge
всякий раз, когда пользователь нажимает на наш компонент.
Управление состоянием с помощью отслеживаемых свойств
Давайте изменим наш класс, добавив метод для переключения размера:
Мы сделали несколько вещей здесь, поэтому давайте разберемся с этим.
Сначала мы добавили декоратор @tracked
к переменной экземпляра isLarge
. Эта аннотация указывает Ember отслеживать эту переменную на наличие обновлений. Всякий раз, когда значение этой переменной изменяется, Ember автоматически перерисовывает любые шаблоны, которые зависят от ее значения.
В нашем случае всякий раз, когда мы присваиваем новое значение this.isLarge
, аннотация @tracked
заставит Ember переоценить условие {{#if this.isLarge}}
в нашем шаблоне и переключиться между двумя блоками соответственно.
Зои поясняет ...
Не волнуйтесь! Если вы ссылаетесь на переменную в шаблоне, но забыли добавить декоратор@tracked
, в режиме разработки вы получите понятную ошибку при изменении ее значения!
Обрабатываем действия пользователя
Затем мы добавили метод toggleSize
в наш класс, который переключает this.isLarge
в противоположность его текущему состоянию ( false
становится true
или true
становится false
).
Наконец, мы добавили декоратор @action
в наш метод. Это указывает Ember, что мы намерены использовать этот метод из нашего шаблона. Без этого метод не будет функционировать должным образом в качестве функции обработки событий (в данном случае обработчика кликов).
Зои поясняет ...
Если вы забудете добавить декоратор@action
, вы также получите ошибку при нажатии на кнопку в режиме разработки!
Теперь пришло время использовать это в шаблоне:
Мы изменили две вещи.
Во-первых, поскольку мы хотели сделать наш компонент интерактивным, мы переключили содержащий тег с <div>
на <button>
(это важно из соображений accessibility). Используя правильный семантический тег, мы также «бесплатно» получим возможность фокуса и взаимодействие с клавиатурой.
Затем мы использовали модификатор {{on}}
, чтобы прикрепить this.toggleSize
в качестве обработчика нажатия кнопки.
Таким образом, мы создали наш первый интерактивный компонент. Попробуйте как он работает в браузере!
Тестирование взаимодействия с пользователем
Наконец, давайте напишем тест для этого нового поведения:
Результат отработки теста
Давайте почистим наш шаблон, прежде чем двигаться дальше. Мы добавили много дубликатов, когда вставляли условные выражения в шаблон. Если мы посмотрим внимательно, единственные вещи, которые отличаются между этими двумя блоками:
1) Наличие "large"
CSS-класса в <button>
.
2) Текст «Увеличить»
и «Уменьшить»
.
Эти изменения скрыты в большом количестве дублированного кода. Мы можем уменьшить дублирование, используя вместо этого выражение {{if}}
:
Версия выражения {{if}}
принимает два аргумента. Первым аргументом является условие. Второй аргумент — это выражение (expression), которое должно выполнится, если условие истинно.
Необязательно, {{if}}
может принимать в качестве третьего аргумента выражение, которое должно выполнится, если условие ложно. Это означает, что мы могли бы переписать метку кнопки следующим образом:
Является ли это улучшением ясности нашего кода — вопрос вкуса. В любом случае, мы значительно сократили дублирование в нашем коде и сделали важные фрагменты логики более заметными.
Запустите тесты в последний раз, чтобы убедиться, что наш рефакторинг ничего не сломал, и мы будем готовы к следующему вызову!
Повторное использование компонентов
Оставшаяся нереализованная часть в компоненте — это карта, показывающая местоположение жилища, над чем мы будем работать дальше:
При добавлении карты вы узнаете об:
- Управлении конфигурациями на уровне приложений
- Параметризации компонентов с аргументами
- Доступе к аргументам компонента
- Интерполяции (interpolating) значений в шаблонах
- Переопределении (overriding) атрибутов HTML в ...attributes
- Рефакторинге с геттерами и авто-слежением (auto-track)
- Получении значений JavaScript в тестовом контексте
Управление конфигурациями уровня приложения
Мы будем использовать Mapbox API для создания карт для наших арендуемых объектов. Вы можете зарегистрироваться бесплатно и без кредитной карты.
Mapbox предоставляет API статических изображений карт, который обслуживает изображения карт в формате PNG. Это означает, что мы можем сгенерировать соответствующий URL для параметров, которые мы хотим, и отобразить карту, используя стандартный <img>
. Класс!
Если вам интересно, вы можете изучить варианты, доступные в Mapbox, с помощью интерактивной песочницы.
После того, как вы зарегистрировались в службе, возьмите свой общедоступный токен (default public token) и вставьте его в config/environment.js
:
Как следует из названия, config/environment.js
используется для настройки нашего приложения и хранения ключей API, подобных этим. К этим значениям можно получить доступ из других частей нашего приложения, и они могут иметь различные значения в зависимости от текущей среды (environment) (которая может быть разработкой (development), тестированием (test) или продакшнм (production)).
Зои поясняет ...
При желании вы можете создать разные токены доступа Mapbox для использования в разных средах. Как минимум, каждый токен должен иметь область действия «styles:tile», чтобы использовать API статических изображений Mapbox.
После сохранения изменений в нашем файле конфигурации нам нужно будет перезапустить наш сервер разработки, чтобы получить эти изменения файла. В отличие от файлов, которые мы редактировали, config/environment.js
не перезагружается автоматически.
Вы можете остановить сервер, найдя окно терминала, где работает ember server
, затем нажмите Ctrl + C То есть, нажимая клавишу «C» на клавиатуре, одновременно удерживая клавишу «Ctrl». Как только он остановился, вы можете запустить его снова с помощью той же команды ember server
.
$ ember server
building...
Build successful (13286ms) – Serving on http://localhost:4200/
Создание компонента содержащего класс компонента
Добавив в приложение ключ Mapbox API, давайте сгенерируем новый компонент для нашей карты.
$ ember generate component map --with-component-class
installing component
create app/components/map.js
create app/components/map.hbs
installing component-test
create tests/integration/components/map-test.js
Поскольку не каждый компонент обязательно будет иметь определенное поведение, связанное с ним, генератор компонентов по умолчанию не создает для нас JavaScript файл. Как мы видели ранее, мы всегда можем использовать генератор компонента, чтобы добавить его позже.
Однако, в случае нашего компонента <Map>
, мы почти уверены, что нам понадобится файл JavaScript для некоего поведения, которое нам еще предстоит определить! Поэтому мы можем передать флаг --with-component-class
генератору компонентов, чтобы у нас было все необходимое с самого начала.
Зои советует ...
Слишком много печатать? Используйте командуember g component map -gc
. Флаг-gc
обозначает компонент Glimmer (glimmer component), а также сгенерировать класс (generate class)
Параметризация компонентов с помощью аргументов
Давайте начнем с нашего файла JavaScript:
Здесь мы импортируем токен доступа из файла конфигурации и возвращаем его из геттера (getter) token
. Это позволяет нам получить доступ к нашему токену как this.token
как внутри MapComponent
класса, так и в шаблоне компонента. Также важно кодировать (encode) токен, на случай, если он содержит какие-либо специальные символы, которые не безопасны для URL.
Интерполяция значений в шаблонах
Теперь перейдем от JavaScript файла к шаблону:
Во-первых, у нас есть элемент контейнера для стилизации.
Затем у нас есть <img>
для запроса и рендеринга статического изображения карты из Mapbox.
Наш шаблон содержит несколько значений, которых еще не существует — @lat
, @lng
, @zoom
, @width
и @height
. Это аргументы компонента <Map>
которые мы предоставим ему при вызове.
Параметризуя наш компонент с помощью аргументов, мы создали повторно используемый компонент, который можно вызывать из разных частей приложения и настраивать для удовлетворения потребностей в этих конкретных контекстах. Мы уже видели это в действии при использовании компонента <LinkTo>
ранее; нам нужно было указать аргумент @route
чтобы он знал, на какую страницу перейти.
Мы предоставили разумное значение по умолчанию для атрибута alt
на основе значений аргументов @lat
и @lng
. Вы можете заметить, что мы напрямую интерполируем значения в значение атрибута alt
. Ember автоматически объединит эти интерполированные значения в строковое значение, включая выполнение любого необходимого экранирования (escaping) HTML.
Переопределение атрибутов HTML в ...attributes
Затем мы использовали ...attributes
, чтобы позволить вызывающему потребителю дополнительно настроить <img>
, например, передать дополнительные атрибуты, такие как class
, а также переопределить наш атрибут alt
умолчанию более конкретным или дружественным для человека.
Порядок важен! Ember применяет атрибуты в порядке их появления. Присваивая атрибут дефолтное значение alt
первым ( до применения ...attributes
), мы явно предоставляем вызывающему возможность предоставить более специализированный атрибут alt
в соответствии с необходимым вариантом использования.
Поскольку переданный атрибут alt
(если таковой существует) появится после нашего, он переопределит указанное нами значение. С другой стороны, важно, чтобы мы назначили src
, width
и height
после ...attributes, чтобы они не были случайно перезаписаны вызывающим (invoker).
Атрибут src
интерполирует все необходимые параметры в формат URL для API статического изображения карты Mapbox, включая URL-безопасный токен this.token
.
Наконец, поскольку мы используем изображение @2x
"retina", мы должны указать атрибуты width
и height
. В противном случае, <img>
будет отображаться в два раза больше того, что мы ожидали!
Мы добавили довольно много кода в один компонент, поэтому давайте напишем несколько тестов! В частности, мы должны убедиться, что у нас есть тестовое покрытие для поведения переопределенных HTML-атрибутов, которое мы обсуждали выше.
Обратите внимание, что тест помощник hasAttribute
из qunit-dom
поддерживает использование регулярных выражений. Мы использовали эту функцию, чтобы подтвердить, что атрибут src
начинается с https://api.mapbox.com/
(а не проверяли, чтобы он точно совпадал). Это позволяет быть достаточно уверенными в том, что код работает правильно, без излишней детализации в наших тестах.
Скрестив пальцы… Давайте запустим тесты.
Супер, все тесты зеленые! Но значит ли это, что на самом деле это работает как задумано? Давайте выясним это, вызвав компонент <Map>
из шаблона компонента <Rental>
:
А вот и карта!
Зои советует ...
Если изображение карты не удалось загрузить, убедитесь, что вconfig/environment.js
правильныйMAPBOX_ACCESS_TOKEN
. И не забудьте перезапустить серверы разработки и тестирования после редактирования вашего конфигурационного файла! Если же и тогда не получается, не стесняйтесь спросить совета у Ember-сообщества.
Также давайте добавим проверки в тесты <Rental>
чтобы убедиться, что мы успешно отрисовали компонент <Map>
.
Рефакторинг с использованием геттеров и авто-трекинга (auto-track)
На данный момент большая часть нашего шаблона <Map>
посвящена атрибуту src
тега <img>
, который становится довольно длинным. Одна альтернатива — переместить это вычисление в отдельный JavaScript класс.
Внутри нашего класса JavaScript у нас есть доступ к аргументам нашего компонента с помощью API this.args.*
. Используя это, мы можем переместить логику вычисления URL из шаблона в новый геттер.
Зои поясняет ...
this.args
— это API, предоставляемый суперклассом компонента Glimmer. Вы можете встретить другие суперклассы компонентов (например, «классические» компоненты) в существующих (legacy) кодовых базах, которые предоставляют различные API для доступа к аргументам компонентов из JavaScript кода.
Выглядит неплохо! И все наши тесты все также проходят!
Обратите внимание, что мы не отметили наш геттер как @tracked
. В отличие от переменных экземпляра, геттерам нельзя напрямую присвоить новое значение, поэтому для Ember не имеет смысла отслеживать их на предмет изменений.
При этом значения, полученные геттерами, безусловно, могут измениться. В нашем случае значение, полученное нашим геттером src
зависит от значений lat
, lng
, width
, height
и zoom
из this.args
. Всякий раз, когда эти зависимости обновляются, мы ожидаем, что {{this.src}}
из нашего шаблона будет соответствующим образом обновлен.
Ember делает это автоматически отслеживая любые переменные, к которым обращались при вычислении значения геттера. Пока сами зависимости помечены как @tracked
, Ember точно знает, когда нужно сделать неактуальными (invalidate) и повторно перерисовать любые шаблоны, которые могут содержать любые «несвежие» и устаревшие значения получателя. Эта функция также известна как автоматическое отслеживание (auto-track). Все аргументы, к которым можно получить доступ из this.args
(другими словами, this.args.*
), Неявно помечаются как @tracked
суперклассом компонента Glimmer. Поскольку мы наследовали от этого суперкласса, все просто работает (Just works).
Получение значений JavaScript в тестовом контексте
Просто чтобы быть уверенным, мы можем добавить тест для этого поведения:
Используя специальный API для тестирования this.setProperties
, мы можем передавать произвольные значения в наш компонент.
Обратите внимание, что значение здесь не относится к экземпляру компонента. Мы не имеем прямого доступа или изменения внутренних состояний компонента (это было бы очень грубо!).
Вместо этого this
относится к специальному тестовому объекту контекста, к которому мы имеем доступ внутри помощника render
. Это обеспечивает «мост» для передачи динамических значений в форме аргументов при вызове компонента. Это позволяет нам обновлять эти значения по мере необходимости из тестовой функции.
После прохождения всех наших тестов мы готовы двигаться дальше!
Работа с данными
В этой части мы удалим жестко закодированные данные из нашего компонента <Rental>
. В конце концов ваше приложение будет уметь отображать реальные данные, полученные с сервера:
В этой части вы узнаете о:
- Работе с файлами маршрутов
- Возврату локальных данных из хука модели (model hook)
- Доступу к моделям маршрутов из шаблонов
- Подделыванию (mocking) данные сервера с помощью статических файлов JSON
- Извлечению удаленных (remote) данных из хука модели
- Адаптации данных сервера
- Циклам и локальным переменным в шаблонах с
{{#each}}
Работа с файлами маршрутов
До этого момента данные были жестко запрограммированы в наш компонент <Rental>
. Но это, как вы понимаете, временно, поскольку в конечном итоге мы хотим, чтобы наши данные поступали с сервера. Давайте продолжим и перенесем некоторые из этих жестко закодированных значений из компонента в рамках подготовки к этому шагу.
Мы хотим начать работать над тем, чтобы в конечном итоге мы могли получить данные с сервера, а затем отобразить запрошенные данные в виде динамического содержимого из шаблонов. Для этого нам понадобится место, где мы сможем написать код для извлечения данных и загрузки их в маршруты.
Место, где это можно сделать в Ember — файлы маршрутов. Они нам пока не нужны, потому что все наши маршруты по сути просто визуализируют статические страницы до этого момента, но мы сейчас мы это изменим.
Начнем с создания файла маршрута для индекса маршрута. Мы создадим новый файл в app/routes/index.js
со следующим содержимым:
Здесь происходит много всего, чего мы раньше не видели, так что давайте сделаем пояснения. Сначала мы импортируем класс Route в файл. Этот класс используется в качестве отправной точки для добавления функциональности в маршрут, такой как загрузка данных.
Затем мы наследуем от класса Route наш собственный IndexRoute, который мы также экспортируем, чтобы остальная часть приложения могла его использовать.
Возврат локальных данных из модельного хука
Выглядит неплохо. Но что происходит внутри этого класса маршрута? Мы реализовали асинхронный метод под названием model()
. Этот метод также известен как хук модели.
Хук модели отвечает за запрос и подготовку любых данных, которые вам нужны для данного маршрута. Ember автоматически вызовет эту функцию при переходе на этот маршрут, чтобы у вас была возможность запустить собственный код для получения нужных вам данных. Объект, возвращаемый из этого хука, называется моделью для маршрута (что логично).
Обычно это то место, где мы запрашиваем данные с сервера. Поскольку запрос данных обычно является асинхронной операцией, хук модели помечается как async
. Это дает нам возможность использовать ключевое слово await
для ожидания завершения операций извлечения данных. (от переводчика: также надо помнить, что результат асинхронной функции будет обернут в Promise под капотом Javascript)
Мы вернемся к этому позже. В данный момент из модели мы просто возвращаем те же данные, что мы жестко закодировали в компоненте , но в формате объекта JavaScript (также известного как POJO (Plain Old Javascript Object)).
Доступ к моделям маршрутов из шаблонов
Итак, теперь, когда мы подготовили некоторые данные модели для нашего маршрута, давайте использовать их в нашем шаблоне. В шаблонах маршрутов мы можем получить доступ к модели маршрута через переменную @model
. В нашем случае это будет содержать POJO, возвращенный из нашего хука модели.
Чтобы проверить, что это работает, давайте изменим наш шаблон и попробуем отобразить свойство title
из нашей модели:
Если мы посмотрим на нашу страницу в браузере, мы должны увидеть данные нашей модели в виде нового заголовка.
Замечательно!
Хорошо, теперь, когда мы знаем, что у нас есть модель для использования, давайте удалим все, что мы захардкодили до этого! Вместо жесткого кодирования информации об аренде в наш компонент <Rental>
, мы можем передать объект модели.
Давайте попробуем.
Во-первых, давайте передадим нашу модель нашему компоненту <Rental>
в качестве аргумента @rental
. Мы также удалим посторонний <h1>
, который мы добавили ранее чтобы проверить, что все работает:
Передав @model
в компонент <Rental>
в качестве аргумента @rental
, мы получим доступ к нашему объекту модели «Grand Old Mansion» в шаблоне компонента <Rental>
! Теперь мы можем заменить наши жестко запрограммированные значения в этом компоненте, используя значения из нашей модели @rental
.
Поскольку объект модели содержит те же данные, что и ранее жестко запрограммированный «Grand Old Mansion», страница должна выглядеть точно так же, как и до изменения.
Последнее, что нужно сделать: обновить тесты, чтобы отразить это изменение.
Поскольку тесты компонентов предназначены для визуализации и тестирования одного компонента изолированно от остальной части приложения, они не выполняют никакой маршрутизации, что означает, что у нас не будет доступа к тем же данным, которые возвращаются из хука модели.
Поэтому в тесте нашего <Rental>
компонента нам нужно будет подать данные в него другим способом. Мы можем сделать это, используя setProperties
, о котором мы узнали в предыдущей главе .
Обратите внимание, что нам также необходимо обновить вызов компонента <Rental>
в вызове функции render
чтобы он также получал данные через аргумент @rental
. Если мы запустим наши тесты сейчас, они все должны пройти успешно!
Подделывание (mocking) данных сервера с помощью статических файлов JSON
Теперь, когда у нас есть все на своих местах, давайте самую приятную часть, удалив все значения, которые мы раньше захардкодили, из хука модели и на самом деле получим данные с сервера!
В реальном приложении данные, которые мы запросим, скорее всего, будут поступать с удаленного сервера API. Но мы снова немного схитрим, чтобы избежать настройки сервера API только для этого туториала. Вместо этого мы поместим некоторые данные JSON в общую папку. Таким образом, мы будем запрашивать эти данные JSON с помощью регулярных HTTP-запросов — так же, как с реальным сервером API, — но без необходимости писать какую-либо серверную логику. Круто!
Но откуда данные? Вы можете скачать этот файл данных, где мы подготовили некоторые данные в формате JSON и заархивировали их в файл формата .zip. Извлеките его содержимое в папку public
.
Когда вы закончите, ваша папка public
должна теперь иметь следующий вид:
public
├── api
│ ├── rentals
│ │ ├── downtown-charm.json
│ │ ├── grand-old-mansion.json
│ │ └── urban-living.json
│ └── rentals.json
├── assets
│ └── images
│ └── teaching-tomster.png
└── robots.txt
4 directories, 6 files
Вы можете убедиться, что все работает правильно, отрыв в браузере URL http://localhost:4200/api/rentals.json
.
Наш «сервер» теперь запущен и обслуживает наши арендные объекты в виде данных JSON. Прекрасно!
Извлечению удаленных (remote) данных из хука модели
Теперь давайте снова обратим наше внимание на наш хук модели. Нам нужно изменить это так, чтобы мы на самом деле запрашивали данные с сервера.
Что тут происходит?
Прежде всего, мы используем Fetch API браузера для запроса данных JSON из API нашего сервера по адресу /api/rentals.json
, тому же URL, что мы открывали в браузере ранее.
Как упоминалось выше, выборка данных с сервера обычно является асинхронной операцией. Fetch API принимает это во внимание, поэтому fetch
является async
функцией, как и наш хук модели. Чтобы использовать его ответ, нам нужно воспользоваться ключевым словом await
.
Fetch API возвращает объект ответа асинхронно. Получив этот объект, мы можем преобразовать ответ сервера в любой нужный нам формат; в нашем случае мы знали, что сервер отправлял данные в формате JSON, поэтому мы можем использовать метод json()
для соответствующей обработки данных ответа. Обработка данных ответа также является асинхронной операцией, поэтому здесь мы снова воспользуемся ключевым словом await
.
Адаптация серверных данных
Прежде чем идти дальше, давайте на секунду остановимся, чтобы снова посмотреть на данные нашего сервера.
Эти данные соответствуют формату JSON:API, который немного отличается от жестко закодированных данных, которые мы возвращали из хука модели ранее.
Во-первых, формат JSON:API возвращает массив, вложенный под ключом "data"
, а не только данные для одного арендуемого свойства. Это логично; Теперь мы хотим показать весь список арендуемых свойств, которые поступают с нашего сервера, а не только один, поэтому массив объектов арендуемой недвижимости — это то, что нам нужно.
Объекты арендуемой недвижимости, содержащиеся в массиве, также имеют немного другую структуру. Каждый объект данных имеет type
и id
, которые мы не собираемся использовать в нашем шаблоне (пока!). На данный момент единственные данные, которые нам действительно нужны, вложены в ключ attributes
.
Здесь есть еще одно ключевое отличие, которое, возможно, смогут уловить только те, у кого очень острые глаза: в данных, поступающих с сервера, отсутствует свойство type
, которое ранее существовало в нашем жестко-закодированном объекте модели. Свойство type
может быть либо "Standalone" либо "Community", в зависимости от типа арендуемого имущества, который требуется для нашего компонента <Rental>
.
Во второй части этого руководства мы узнаем о более удобном способе использования данных в формате JSON:API. Сейчас мы можем немного трансформировать данные и самостоятельно разобраться с этими различиями в форматах.
Мы можем справиться со всем этим в нашем хуке модели:
После обработки (parsing) данных JSON мы извлекли объект с вложенными attributes
, вручную добавили атрибут отсутствующего type
, а затем вернули его из хука модели. Таким образом, остальная часть нашего приложения даже не будет знать, что эта разница когда-либо существовала.
Потрясающие! Теперь мы в деле.
Циклы и локальные переменные в шаблонах с помощником (helper) {{#each}}
Последнее изменение, которое нам нужно сделать, — это изменить шаблон маршрута index.hbs
, где мы вызываем наши компоненты <Rental>
. Ранее мы передавали @rental
как @model
нашим компонентам. Тем не менее, @model
больше не является единственным объектом — это массив (почти)! Итак, нам нужно изменить шаблон, чтобы учесть эту разницу.
Посмотрим как.
Мы можем использовать синтаксис {{#each}}...{{/each}}
для итерации и циклическому проходу по массиву, возвращаемому хуком модели. Для каждой итерации массива — для каждого элемента в массиве — мы будем визуализировать переданный ему блок. В нашем случае блок — это наш компонент <Rental>
, окруженный тегами <li>
.
Внутри блока у нас есть доступ к элементу текущей итерации с помощью переменной {{rental}}
. Но почему rental
? Ну, потому что мы его так назвали! Эта переменная происходит от as |rental|
при объявлении each
цикла. Мы могли бы так же легко назвать её как-нибудь еще, например as |property|
, в этом случае мы должны были бы получить доступ к текущему элементу через переменную {{property}}
.
Теперь давайте перейдем к нашему браузеру и посмотрим, как выглядит наш индексный маршрут с этим изменением.
Ура! Наконец, мы видим различные арендуемые объекты в нашем списке. И мы всего лишь немного поиграли с fetch
и написали цикл. Довольно продуктивно, не правда ли?
И что характерно, все наши тесты также проходят!
Резюме первой части (точнее частей 1.1 и 1.2)
К этому моменту вы уже знаете основные моменты фреймворка и хорошо подготовлены для выполнения самых разнообразных задач по разработке в Ember!
Сделайте перерыв или поэкспериментируйте с созданием собственного уникального приложения Ember, используя только что приобретенные навыки.
Не стесняйтесь делиться успехами в твиттере используя хэштег #Emberjs. Еще можно посетить Discord канал сообщества, а также русскоязычный телеграмм канал ember_js
Когда вы вернетесь, мы будем опираться на то, что узнали в первой части, и перейдем на новый уровень!
UPDATE: И сноса благодарю пользователя MK1301 за коррекцию ошибок.