Ember.js — идеальный фреймворк для веб приложений

Перевод статьи Graham Cox:Ember.js: The Perfect Framework for Web Applications.

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

Мы собираемся написать приложение Dice Roller, позволяющее кинуть кости и посмотреть историю всех совершенных бросков. Полностью работающие приложение можно увидеть на GitHub.

Ember.js вобрал в себя множество современных JavaScript концепций и технологий. Вот их неполный список:

  • Транспайлер Babel для полноценной поддержки ES2015 синтаксиса.
  • Поддержка юнит, интеграционного и приемочного тестирований с помощью Testem и QUnit.
  • Brocolli.js для сборки ассетов.
  • Поддержка live-reload для сокращения отклика во время разработки.
  • Шаблонизация с использованием Handlebars.
  • Навигация в любую часть приложения благодаря системе роутинга.
  • Полная поддержка JSON API, но при этом присутствует возможность использовать любой API, который вам необходим.

Для работы с Ember.js предполагается, что у вас установлены свежие версии Node.js и npm. Если нет, то их можно скачать и установить с сайта Node.js.

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

Знакомство с ember-cli

Немало возможностей Ember.js связано с его интерфейсом командной строки или CLI. Этот инструмент, известный как ember-cli, управляет большой частью процесса разработки: от создания приложения и добавления различного функционала и до запуска тестов и деплоя на продакшен.

Практически всё во время разработки Ember.js приложения будет в какой-то степени связано с этим инструментом, поэтому важно понимать как им пользоваться. Мы будем использовать его при создании нашего приложения.

Первым делом нужно убедиться, что ember-cli установлен и актуален. Устанавливаем его c помощью npm:

$ npm install -g ember-cli

и проверяем успешность установки:

$ ember --version
ember-cli: 2.15.0-beta.1
node: 8.2.1
os: darwin x64

Создание вашего первого Ember.js приложения

С установленным ember-cli мы готовы приступить к созданию нашего приложения. В первый раз мы будем использовать ember-cli для того, чтобы создать всю структуру приложения и сделать первоначальные настройки.

Наше приложение создалось и готово. У нас даже настроился Git, как система контроля версий.

Примечание: Вы можете отключить интеграцию с Git, а также использовать Yarn вместо npm. Используйте команду ember new --help для более подробной информации.

Запуск сервера Ember.js приложения для целей разработки делается также с помощью ember-cli:

Всё готово. Приложение запущено по адресу http://localhost:4200 и выглядит следующим образом:

Также запустился LiveReload сервер, автоматически наблюдающий за изменениями в файловой системе. Это значит, что любое изменение в коде приведёт к автоматической перезагрузке нашего приложения в браузере. А изменения картинок или CSS применяется даже без перезагрузки.

Начальная страница подсказывает нам, что делать. Давайте изменим её и посмотрим, что произойдет. Мы собираемся изменить файл app/templates/application.hbs, чтобы он выглядел так:

Примечание: {{outlet}} — это часть того, как роутинг работает в Ember. Мы еще вернемся к этому позже.

Первое, на что стоит обратить внимание, это результат работы ember-cli:

Это говорит нам о том, что сервер заметил наши изменения в шаблоне, перезапустил всё без каких-либо действий с нашей стороны.

Теперь давайте посмотрим в браузер. LiveReload обновил страницу браузера с примененными изменениями и нам также ничего не нужно больше делать.

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

В дополнение, у нас уже все готово для запуска тестов. Для этого мы вновь можем воспользоваться ember-cli таким образом:

Заметьте, что в выводе консоли упоминается PhantomJS. Это потому, что Ember имеет полную поддержку запуска интеграционных тестов в браузере и по умолчанию использует безголовый браузер PhantomJS (прим. пер., с версии 2.15 по умолчанию используется headless chrome). Вы можете настроить запуск тестов в любом браузере по вашему желанию. Например при настройке непрерывной интеграции (CI) будет полезно воспользоваться этим, чтобы удостовериться, что приложение корректно работает во всех поддерживаемых вами браузерах.

Как устроено Ember.js приложение

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

В корне директории вы можете увидеть следующие файлы и папки:

  • README.md — стандартный файл, описывающий ваше приложение.
  • package.json — стандартный файл пакетного менеджера npm, также описывающий ваше приложение, но с точки зрения зависимостей. В нем описаны зависимости вашего приложения и их версии, чтобы они устанавливались корректно.
  • ember-cli-build.js — конфигурационный файл для ember-cli.
  • testem.js — конфигурационный файл для тестирующего фреймворка Testem. Он позволяет, среди прочего, определить в каких браузерах будут запускаться тесты.
  • app/ — здесь хранится логика приложения. Многое происходит в этой папке и мы рассмотрим это ниже.
  • config/ — конфигурация нашего приложения:
  • config/targets.js — содержит список поддерживаемых браузеров. Это необходимо для Babel, чтобы он транспилировал ваш код в работающий во всех необходимых вам браузерах.
  • config/environment.js — содержит необходимые настройки приложения, отличающиеся в различных окружениях.
  • public/ — любые статические ресурсы, которые необходимы вашему приложению. Например, картинки или шрифты.
  • vendor/ — сюда можно сложить любые зависимости, которые не будут управляться системой сборки.
  • tests/ — это место для тестов:
  • tests/unit — все юнит тесты приложения.
  • tests/integration — все интеграционные тесты.

Общая структура страницы

Пока мы не зашли слишком далеко, давайте добавим на нашу страницу немного разметки. А чтобы она смотрелась хорошо, будем использовать Materialize CSS framework.

Добавление подобного стороннего контента может быть осуществленно несколькими способами:

  • Указанием ссылки на внешний CDN сервис
  • С помощью пакетных менеджеров вроде npm или Bower
  • Подключив напрямую в приложение из папки vendor/
  • Использовать Ember Addon, если такой имеется

К сожалению, аддон для Materialize пока что не совместим с последней версией Ember, поэтому мы просто добавим ссылку на него. Чтобы сделать это, мы обновим app/index.html файл, являющийся корневой страницей, в которую будет отрендерено наше приложение. Мы хотим добавить ссылки на CDN для jQuery, Google Icon Font и Materialize.

Теперь мы можем обновить нашу главную страницу, добавив разметки. Для этого отредактируем файл app/templates/application.hbs:

Мы добавили шапку, а также контейнер с тэгом {{outlet}} упомянутым ранее.

В браузере это должно выглядеть так:

Так, что же такое outlet тэг? Ember работает на основе роутов, где каждый роут является ребенком другого роута. Самый верхний роут в этой иерархии обрабатывается Ember автоматически и рендерит шаблон app/templates/application.hbs.

Тэг outlet определяет, где Ember отрендерит следующий роут в текущей иерархии. Таким образом роут первого уровня будет отрендерен в добавленный нами тэг в application.hbs, а роут второго уровня будет отрендерен в такой же тэг в темплейте роута первого уровня. И так далее.

Создание нового роута

В Ember.js приложении каждая посещаемая страница доступна с помощью роута. Существует прямая связь между адресной строкой (URL) в браузере и роутом в приложении.

Проще показать это на примере. Давайте добавим новый роут в наше приложение, позволяющий пользователю бросать кости. И вновь это делается с помощью ember-cli:

Что создалось с помощью этой команды?

  • Обработчик для роута — app/routes/roll.js
  • Шаблон для роута — app/templates/roll.hbs
  • Тест для роута tests/unit/routes/roll-test.js
  • Новый роут добавился в файл конфигурации роутера — app/router.js

Давайте посмотрим на это в действии. Для начала мы хотим создать довольно простую страницу, позволяющую нам получить число после броска костей. Для этого обновим файл app/templates/roll.hbs:

Результат будет доступен в браузере по адресу http://localhost:4200/roll:

Теперь нам нужна возможность попадать на эту страницу с главной страницы. Ember позволяет сделать это очень просто с помощью хелпера link-to, который принимает первым аргументом имя роута и рендерится в разметку, позволяющую нашему пользователю попасть на нужный роут.

В нашем случае нужно обновить файл app/templates/application.hbs, чтобы он содержал следующее:

Это добавит ссылку на роут roll в шапку страницы, как мы и задумывали:

Создание модульных компонент

Если на данном этапе вы попробовали протестировать наше приложение, то могли заметить одну проблему. Переход по ссылке в роут roll работает корректно, но метки элементов форм не выстраиваются правильно. Это происходит потому что Materialize нужно использовать JavaScript, чтобы поставить метки на свои места, после того как они будут отрендерены. Но динамический роутинг предполагает, что страница не будет перезагружена. Таким образом, у нас пока что нет места, где можно инициализировать этот JavaScript код.

На помощь приходят компоненты. Компоненты — это частички интерфейса пользователя, имеющие свой жизненный цикл, с которым мы можем взаимодействовать и в который мы можем встроить необходимый нам JavaScript код. Также они используются для создания переиспользуемых элементов интерфейса, но к этому мы ещё вернёмся.

Пока что мы собираемся создать компонент, представляющий собой форму для броска костей. Как обычно, создание компонента тоже можно осуществить с помощью ember-cli:

$ ember generate component roll-dice
installing component
create app/components/roll-dice.js
create app/templates/components/roll-dice.hbs
installing component-test
create tests/integration/components/roll-dice-test.js

Мы получили:

  • app/components/roll-dice.js — код, управляющий компонентом
  • app/templates/components/roll-dice.hbs — шаблон компонента, где мы определим, как он выглядит
  • tests/integration/components/roll-dice-test.js — тест, чтобы удостовериться, что компонент работает правильно

Мы перенесём всю разметку из роута roll в компонент, что не повлияет на работу приложения в целом, но позволит нам использовать всю силу компонент.

Обновим шаблон компонента app/templates/components/roll-dice.hbs:

А также шаблон роута app/templates/roll.hbs:

Тэг roll-dice в шаблоне говорит Ember, где отрендерить наш компонент.

Жизненный цикл компонента

Компоненты в Ember имеют заданный жизненный цикл, которому они следуют, с множеством хуков, вызываемых на различных стадиях. Мы используем хук didRender, который будет вызван после того, как компонент будет отрендерен (в первый раз или в любые последующие), чтобы попросить Materialize обновить метки элементов форм.

Сделать это можно, обновив код компонента app/components/roll-dice.js таким образом:

Теперь при любом заходе в роут roll нужный нам код отработает и Materialize исправит отображение меток.

Связывание данных

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

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

В нашем случае у нас есть три поля ввода, поэтому нам нужно добавить три строчки внутри класса компонента в файле app/components/roll-dice.js:

Затем мы обновим наш шаблон, заменив HTML разметку полей ввода на специальные хелперы.

Обратите внимание, что атрибут value имеет слегка странный синтаксис. Подобный синтаксис может быть использован с любым атрибутом хелпера, не только с value. Вообще существует три способа передачи значения в атрибут:

  • Как строку в кавычках (значением будет эта строка
  • Как строку без кавычек (в этом случае значение будет взято из одноименного поля в классе компоненты, но компонента никогда не обновится)
  • С использованием (mut <name>) (значение также возьмется из одноименного поля в классе компонента, но компонент будет изменяться, когда значение изменится в браузере)

Всё вышеизложенное означает, что теперь мы можем обращаться к добавленным в компонент полям как к значениям полей ввода, а всё остальное сделает Ember.

Экшены компонент

Следующим шагом мы хотим добавить компоненту интерактивность. Например, хорошо бы обрабатывать нажатие кнопки «Бросить кость». Для этого в Ember есть экшены. Это методы, описанные в классе компонента, которые могут быть подключены в шаблон компонента. Обычно методы описывают в специальном объекте компонента actions.

Добавим экшен в наш компонент app/components/roll-dice.js:

Мы возвращаем false, чтобы предотвратить всплытие события. Это довольно стандартная процедура в веб-приложениях и в данном случае предотвращает отправку формы и перезагрузку страницы.

Вы можете заметить, что мы ссылаемся на поля, которые мы ранее объявили в классе, чтобы иметь доступ к значениям полей ввода. Здесь нет никакого взаимодействия с DOM: мы оперируем только JavaScript переменными.

Осталось подключить наш экшен. В шаблоне нам нужно сказать тэгу формы, что ему нужно вызвать triggerRoll экшен, когда случится событие onsubmit. Это делается добавлением всего одного атрибута с использованием action хелпера. В шаблоне app/templates/components/roll-dice.hbs это выглядит так:

Заполнив поля формы и нажав на кнопку, мы увидим диалоговое окно с результатом нашего ввода.

Взаимодействие с сервером

Следующим шагом будет написание логики настоящего броска кости. Это требует взаимодействия с сервером, так как сервер ответственен за запоминание результата броска кости.

Мы хотим достичь следующего:

  • Пользователь определяет параметры для броска кости
  • Пользователь нажимает на кнопку «Бросить кость»
  • Приложение выполняет логику броска кости и отправляет результат и параметры совершенного броска на сервер
  • Сервер запоминает результат и сообщает клиенту об удачном сохранении
  • Браузер отображает результат броска

Звучит довольно просто. И конечно, с Ember, это действительно так.

Ember управляет этим, используя встроенную концепцию хранилища — Store, наполненного моделями - Models. Хранилище — единственный источник знаний о данных во всем приложении, а модель представляет часть этих данных в хранилище. Модели сами знают как сохранить себя на сервер, а хранилище знает как создавать и управлять моделями.

Передача управления от компонент в роуты

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

Это потому что роуты отвечают за определенную бизнес-логику в приложении, в то время как компоненты представляют собой маленькие кусочки интерфейса. Чтобы всё работало как надо, компоненты способны посылать сигналы вверх по иерархии вложенности, когда происходит какое-либо событие. Это очень похоже на то, как хелперы в шаблоне компонента посылают сигналы в компонент, когда что-то произошло.

Для начала переместим логику показа диалогового окна из компонента в роут. Для этого нам нужно изменить некоторые части нашего кода.

В классе, ответственном за управление роутом app/routes/roll.js зарегистрируем экшен saveRoll, которым мы собираемся выполнить:

И перепишем логику экшена компонента. Теперь мы хотим вызвать другой экшен в нашем компоненте, передав параметры броска в его аргументы. Это делается с помощью метода sendAction, доступного в классе компонента.

Осталось связать экшен из роута и экшен компонента. Для этого изменим внешний вид вызова компонента в шаблоне роута app/templates/roll.hbs:

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

И вновь, всё сделанное нами никак не повлияло на работу приложения, но теперь все части в правильных местах.

Сохранение в хранилище

Прежде чем мы сохраним данные в хранилище, нам необходимо определить модель, представляющую эти данные. Используя наш надежный инструмент ember-cli, создадим структуру модели и затем заполним её.

Чтобы создать модель, выполним команду:

Теперь мы можем заполнить модель app/models/roll.js атрибутами, необходимыми для представления наших данных:

DS.attr вызывается чтобы определить атрибут модели заданного типа. Эти типы в Ember называются преобразователями (transform). Варианты преобразователей по умолчанию: "string", "number", "data"или "boolean". Но при необходимости вы всегда можете добавить свой.

Используем эту модель для создания броска кости в хранилище и сохранения на бэкенд. Для этого нам нужно получить доступ к хранилищу в классе роута app/routes/roll.js:

Если мы сейчас попробуем выполнить этот код, нажав на кнопку «Бросить кость», это приведёт к сетевому вызову на наш сервер. И это не сработает, потому что у нас нет сервера.

Не будем беспокоиться об этом, потому что мы не затрагиваем здесь тему реализации бэкенда. Но если вам нужно разрабатывать Ember приложение совсем без бэкенда, есть различные варианты, например, использование аддона ember-localstorage-adapter, чтобы сохранять данные в local-storage браузера. Также можно использовать аддоны ember-cli-mirage или emberfire.

Загрузка из хранилища

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

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

Так как index роут уже существует, то нам не нужно вызывать никаких команд с помощью ember-cli, мы просто создадим файл app/routes/index.js, в который добавим:

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

Создадим файл app/templates/index.hbs и добавим в него разметку:

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

Заключение

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

Использование Ember может очень сильно повысить эффективность разработки фронтенда. В противоположность библиотекам, вроде React, Ember даёт вам весь необходимый функционал без дополнительных усилий. Использование ember-cli и настроенных процессов сборки приложения выводит его на следующий уровень, делая процесс невероятно простым и безболезненным от начала и до конца. Добавив сюда поддержку сообщества практически не остается задач, которые не могли бы быть решены.

К сожалению, может быть сложно использовать Ember вместе с уже существующим проектом. Это работает прекрасно для старта нового проекта. Также Ember работает из коробки только с несколькими вариантами API бэкенда и если ваш бэкенд не соответствует им, то вы можете потратить много времени либо переписывая бекенд, либо настраивая Ember на работу с вашим API.

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


Слушайте наш подкаст в iTunes и SoundCloud, читайте нас на Medium, контрибьютьте на GitHub, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook.

Статья на GitHub