Это будет программа Dice Roller, которая позволяет «бросать» игральные кости, предварительно задавая их параметры, и просматривать историю предыдущих «бросков». Её код можно найти на Github.
Фреймворк Ember.js включает в себя множество современных концепций и технологий из мира JavaScript. Среди его возможностей хотелось бы отметить следующие:
- Использование транспилятора Babel для обеспечения поддержки ES2016.
- Поддержка средств тестирования Testem и QTest, что открывает возможности по модульному, интеграционному и приёмочному тестированию.
- Поддержка сборщика Broccoli.js.
- Интерактивная перезагрузка веб-страниц, что ускоряет процесс разработки.
- Поддержка шаблонизатора Handlebar.
- Использование модели разработки, при реализации которой в первую очередь создаются URL-маршруты. Это обеспечивает полную поддержку глубоких ссылок.
- Наличие слоя для работы с данными, основанного на JSON API, но поддерживающего подключение к любому API, с которым нужно наладить работу.
Для того, чтобы приступить к работе с Ember.js, понадобятся свежие версии Node.js и npm. Всё это можно загрузить с сайта Node.js.
Кроме того, стоит сказать, что Ember.js — это фреймворк, ориентированный исключительно на фронтенд. Он поддерживает множество способов взаимодействия с различными бэкендами, но всё, что относится к серверному коду, не входит в сферу ответственности Ember.
Знакомство с ember-cli
Интерфейс командной строки Ember.js,
ember-cli
, открывает доступ ко множеству возможностей этого фреймворка. Ember-cli
поддерживает программиста на всех этапах работы. Он упрощает создание приложения, расширение его функциональности, тестирование и запуск проекта в режиме разработки.Практически всё, чем вы будете заниматься в ходе создания Ember-приложения, будет, в определённой степени, включать в себя использование
ember-cli
. Поэтому важно изучить этот инструмент. Мы, в ходе работы над учебным проектом, будем постоянно пользоваться им.Первый шаг нашей работы заключается в установке
ember-cli
, или, если он уже установлен — в проверке актуальности имеющейся версии. Установить ember-cli
можно из реестра npm
с помощью следующей команды:$ npm install -g ember-cli
Для того, чтобы проверить, успешно ли прошла установка, а заодно узнать то, какая версия
ember-cli
установлена, можно воспользоваться следующей командой:$ ember --version
ember-cli: 2.15.0-beta.1
node: 8.2.1
os: darwin x64
Создание первого приложения на Ember.js
После того, как
ember-cli
установлен, мы готовы к тому, чтобы приступить к созданию приложения. Это — первая ситуация, в которой мы будем пользоваться ember-cli
. Он создаёт структуру приложения, настраивает его и даёт нам работающий проект. Создадим приложение dice-roller
следующей командой:$ ember new dice-roller
installing app
create .editorconfig
create .ember-cli
create .eslintrc.js
create .travis.yml
create .watchmanconfig
create README.md
create app/app.js
create app/components/.gitkeep
create app/controllers/.gitkeep
create app/helpers/.gitkeep
create app/index.html
create app/models/.gitkeep
create app/resolver.js
create app/router.js
create app/routes/.gitkeep
create app/styles/app.css
create app/templates/application.hbs
create app/templates/components/.gitkeep
create config/environment.js
create config/targets.js
create ember-cli-build.js
create .gitignore
create package.json
create public/crossdomain.xml
create public/robots.txt
create testem.js
create tests/.eslintrc.js
create tests/helpers/destroy-app.js
create tests/helpers/module-for-acceptance.js
create tests/helpers/resolver.js
create tests/helpers/start-app.js
create tests/index.html
create tests/integration/.gitkeep
create tests/test-helper.js
create tests/unit/.gitkeep
create vendor/.gitkeep
NPM: Installed dependencies
Successfully initialized git.
$
Выполнение вышеприведённой команды приводит к созданию работоспособного макета приложения. Она даже настраивает систему контроля версий Git. Обратите внимание на то, что интеграцию с Git можно и отключить, кроме того, вместо менеджера пакетов
npm
можно использовать yarn
. Подробности об этом можно найти в документации к Ember.Теперь посмотрим на то, что у нас получилось. Запуск Ember-приложения для целей разработки, как уже было сказано, выполняется с использованием
ember-cli
. Делается это так:$ cd dice-roller
$ ember serve
Livereload server on http://localhost:49153
'instrument' is imported from external module 'ember-data/-debug' but never used
Warning: ignoring input sourcemap for vendor/ember/ember.debug.js because ENOENT: no such file or directory, open '/Users/coxg/source/me/writing/repos/dice-roller/tmp/source_map_concat-input_base_path-2fXNPqjl.tmp/vendor/ember/ember.debug.map'
Warning: ignoring input sourcemap for vendor/ember/ember-testing.js because ENOENT: no such file or directory, open '/Users/coxg/source/me/writing/repos/dice-roller/tmp/source_map_concat-input_base_path-Xwpjztar.tmp/vendor/ember/ember-testing.map'
Build successful (5835ms) – Serving on http://localhost:4200/
Slowest Nodes (totalTime => 5% ) | Total (avg)
----------------------------------------------+---------------------
Babel (16) | 4625ms (289 ms)
Rollup (1) | 445ms
Теперь всё готово, приложение доступно по адресу http://localhost:4200 и выглядит оно так:
Помимо самого приложения запускается сервис LiveReload, который наблюдает за изменениями файлов проекта и автоматически перезагружает страницу в браузере. Это означает, что в ходе модификации сайта вы очень быстро увидите результат изменений.
Первая страница сообщает нам о том, что можно сделать дальше, прислушаемся к этим сообщениям, изменим главную страницу и посмотрим, что из этого получится. Для этого приведём файл
app/templates/application.hbs
к такому виду:This is my new application.
{{outlet}}
Обратите внимание на то, что тег
{{outlet}}
является частью того, как в Ember работает процесс маршрутизации. Подробнее об этом мы поговорим ниже.После изменения файла сразу стоит посмотреть на то, что выведет в консоль
ember-cli
. Это должно выглядеть примерно так:file changed templates/application.hbs
Build successful (67ms) – Serving on http://localhost:4200/
Slowest Nodes (totalTime => 5% ) | Total (avg)
----------------------------------------------+---------------------
SourceMapConcat: Concat: App (1) | 9ms
SourceMapConcat: Concat: Vendor /asset... (1) | 8ms
SimpleConcatConcat: Concat: Vendor Sty... (1) | 4ms
Funnel (7) | 4ms (0 ms)
Здесь нам сообщают, что система обнаружила изменение шаблона, перестроила и перезапустила проект. Это полностью автоматический процесс, мы в нём участия не принимаем.
Теперь посмотрим в браузер. Если у вас установлен и запущен
LiveReload
, то вам даже не нужно вручную перезагружать страницу для того, чтобы увидеть изменения. В противном случае страницу приложения придётся перезагрузить самостоятельно.Нельзя сказать, что внешний вид страницы поражает воображение, но то, что мы с ней сделали, потребовало минимума усилий с нашей стороны.
Кроме того, у нас имеется полностью готовая к использованию подсистема тестирования. Для того, чтобы её запустить, надо воспользоваться соответствующим средством Ember:
$ ember test
? Building'instrument' is imported from external module 'ember-data/-debug' but never used
? BuildingWarning: ignoring input sourcemap for vendor/ember/ember.debug.js because ENOENT: no such file or directory, open '/Users/coxg/source/me/writing/repos/dice-roller/tmp/source_map_concat-input_base_path-S8aQFGaz.tmp/vendor/ember/ember.debug.map'
? BuildingWarning: ignoring input sourcemap for vendor/ember/ember-testing.js because ENOENT: no such file or directory, open '/Users/coxg/source/me/writing/repos/dice-roller/tmp/source_map_concat-input_base_path-wO8OLEE2.tmp/vendor/ember/ember-testing.map'
cleaning up...
Built project successfully. Stored in "/Users/coxg/source/me/writing/repos/dice-roller/tmp/class-tests_dist-PUnMT5zL.tmp".
ok 1 PhantomJS 2.1 - ESLint | app: app.js
ok 2 PhantomJS 2.1 - ESLint | app: resolver.js
ok 3 PhantomJS 2.1 - ESLint | app: router.js
ok 4 PhantomJS 2.1 - ESLint | tests: helpers/destroy-app.js
ok 5 PhantomJS 2.1 - ESLint | tests: helpers/module-for-acceptance.js
ok 6 PhantomJS 2.1 - ESLint | tests: helpers/resolver.js
ok 7 PhantomJS 2.1 - ESLint | tests: helpers/start-app.js
ok 8 PhantomJS 2.1 - ESLint | tests: test-helper.js
1..8
# tests 8
# pass 8
# skip 0
# fail 0
# ok
Обратите внимание на то, что основная масса выводимых данных поступает от Phantom.js. Так происходит из-за того, что здесь имеется полная поддержка интеграционного тестирования, которое, по умолчанию, производится в браузере PhantomJS без графического интерфейса. Присутствует и возможность запускать тестирование в других браузерах, если в этом есть необходимость. Настраивая систему непрерывной интеграции (CI, Continuous Integration), стоит воспользоваться этой возможностью для того, чтобы убедиться в том, что приложение правильно работает во всех поддерживаемых браузерах.
Структура Ember.js-приложения
Прежде чем мы займёмся работой над приложением, разберёмся со структурой папок и файлов, на которой оно основано. Вышеприведённая команда
ember new
создаёт необходимые приложению папки и файлы, которых довольно много. Понимание того, как это всё устроено, важно для организации эффективной работы с Ember и для создания проектов любого масштаба.На самом верхнем уровне структуры приложения можно обратить внимание на следующие файлы и папки:
- README.md – стандартный readme-файл с описанием приложения.
- package.json – конфигурационный файл
npm
, описывающий приложение. Он, в основном, используется для того, чтобы обеспечить корректную установку зависимостей.
- ember-cli-build.js – конфигурационный файл для
ember-cli
, который отвечает за сборку приложения.
- testem.js – Это конфигурационный файл для подсистемы тестирования. Он позволяет, кроме прочего, задавать браузеры, которые следует использовать для организации испытаний приложения в разных средах.
- app/ – тут хранится логика приложения. В этой папке происходит много всего интересного, ниже мы об этом поговорим.
- config/ – здесь находятся настройки приложения.
- public/ – тут хранятся статические ресурсы, которые нужно включить в приложение. Например, это изображения и шрифты.
- vendor/ – сюда попадают зависимости фронтенда, которыми не управляет система сборки.
- tests/ – здесь расположены тесты.
Структура страницы
Прежде чем продолжать, займёмся структурой страницы. В данном случае мы будем пользоваться фреймворком Materialize CSS, что позволяет придать странице приятный вид и сделать работу с ней удобнее для конечного пользователя.
Для того, чтобы добавить в приложение поддержку дополнительных средств, можно воспользоваться одним из следующих подходов:
- Подключить то, что нужно, напрямую, воспользовавшись внешней службой вроде какой-нибудь CDN.
- Использовать менеджер пакетов наподобие
npm
илиBower
для установки необходимого пакета.
- Включить необходимые ресурсы непосредственно в приложение.
- Использовать подходящий аддон для Ember.
К сожалению, аддон для Materialize пока не работает с самой свежей версией Ember.js, поэтому мы подключим этот фреймворк на главной странице приложения, пользуясь ссылкой на CDN-ресурс. Для того, чтобы это сделать, нужно отредактировать файл
app/index.html
, который описывает структуру главной страницы, в которой выводится приложение. Мы собираемся добавить CDN-ссылки на jQuery, на шрифт с иконками Google, и на Materialize.<!-- Inside the Head section -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.100.1/css/materialize.min.css">
<!-- Inside the Body section -->
<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.100.1/js/materialize.min.js"></script>
Теперь можно изменить главную страницу таким образом, чтобы она отображала базовый шаблон приложения. Делается это путём приведения файла
app/templates/application.hbs
к такому виду:<nav>
<div class="nav-wrapper">
<a href="#" class="brand-logo">
<i class="material-icons">filter_6</i>
Dice Roller
</a>
<ul id="nav-mobile" class="right hide-on-med-and-down">
</ul>
</div>
</nav>
<div class="container">
{{outlet}}
</div>
Благодаря этому коду в верхней части страницы окажется навигационная панель, подготовленная средствами Materialize. На странице имеется и контейнер, в котором расположен упомянутый выше тег
{{outlet}}
.Теперь, если заглянуть в браузер, можно обнаружить, что страница выглядит так, как показано на рисунке ниже.
Пришло время поговорить о теге
{{outlet}}
. Работа Ember основана на маршрутах. Каждый маршрут считается потомком какого-то другого маршрута. Корневой маршрут обрабатывается автоматически, он выводит шаблон app/templates/application.hbs
.Тег
{{outlet}}
задаёт место, где Ember выведет содержимое, соответствующее следующему маршруту в текущей иерархии — в итоге маршрут первого уровня выводится в этот тег в application.hbs
, маршрут второго уровня выводится в таком же теге в шаблоне первого уровня, и так далее.Создание нового маршрута
Доступ к каждой странице приложения на Ember.js организован через маршрут (Route). Существует прямое соответствие между URL, который открывает браузер, и материалами, относящимися к маршруту, которые выводит на экран приложение.
Легче всего ознакомиться с этой концепцией на примере. Добавим в приложение новый маршрут, который позволяет пользователю «бросать» игральные кости. Этот шаг, опять же, выполняется с помощью
ember-cli
:$ ember generate route roll
installing route
create app/routes/roll.js
create app/templates/roll.hbs
updating router
add route roll
installing route-test
create tests/unit/routes/roll-test.js
Вот что было создано благодаря вызову этой команды:
- Обработчик для маршрута —
app/routes/roll.js
- Шаблон для маршрута —
app/templates/roll.hbs
.
- Тест для маршрута —
tests/unit/routes/roll-test.js
- Обновление конфигурации роутера, дающее ему сведения об этом новом маршруте —
app/router.js
Посмотрим на то, как это работает. Пока у нас будет очень простая страница, содержащая элементы, описывающие игральную кость и кнопку, которая, немного позже, позволит её «бросить». Для того, чтобы сформировать эту страницу, поместим следующий код в файл шаблона
app/templates/roll.hbs
:<div class="row">
<form class="col s12">
<div class="row">
<div class="input-field col s12">
<input placeholder="Name" id="roll_name" type="text" class="validate">
<label for="roll_name">Name of Roll</label>
</div>
</div>
<div class="row">
<div class="input-field col s6">
<input placeholder="Number of dice" id="number_of_dice" type="number" class="validate" value="1">
<label for="number_of_dice">Number of Dice</label>
</div>
<div class="input-field col s6">
<input placeholder="Number of sides" id="number_of_sides" type="number" class="validate" value="6">
<label for="number_of_sides">Number of Sides</label>
</div>
</div>
<div class="row">
<button class="btn waves-effect waves-light" type="submit" name="action">
Roll Dice
<i class="material-icons right">send</i>
</button>
</div>
</form>
</div>
{{outlet}}
После этого посетим страницу http://localhost:4200/roll и посмотрим что получится.
Теперь нам нужен способ организации перехода на эту страницу через интерфейс приложения. Ember упрощает решение этой задачи благодаря использованию тега
link-to
. Он, кроме прочего, принимает имя маршрута, по которому мы отправляем пользователя, и выводит элементы, позволяющие пользователю перейти по этому маршруту.Включим в файл
app/templates/application.hbs
следующее:<ul id="nav-mobile" class="right hide-on-med-and-down">
{{#link-to 'roll' tagName="li"}}
<a href="roll">Roll Dice</a>
{{/link-to}}
</ul>
Это приведёт к тому, что навигационная панель в верхней части страницы примет вид, показанный на рисунке ниже.
В правой части панели появилась новая ссылка,
Roll Dice
, при щелчке по которой пользователь переходит по маршруту /roll
. Именно этого мы и пытались добиться.Разработка модульных компонентов
Если вы попробуете поработать с приложением, испытать его, вы заметите проблему. Домашняя страница открывается нормально, ссылка
/roll
работает, но подписи полей на форме выровнены неправильно. Так происходит из-за того, что Materialize надо вызвать определённый JS-код для того, чтобы соответствующим образом расположить элементы, но, из-за особенностей динамической маршрутизации, страницы не перезагружаются. Сейчас мы это исправим.Поработаем с компонентами. Компоненты — это фрагменты пользовательского интерфейса, имеющие полный жизненный цикл, с которыми можно взаимодействовать. Кроме того, используя компоненты, можно, при необходимости, создавать элементы пользовательского интерфейса, подходящие для повторного использования. Мы поговорим об этом позже.
Сейчас мы собираемся создать единственный компонент, представляющий собой форму
Roll Dice
. Как обычно в подобных ситуациях, для создания компонента обратимся к 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
Теперь мы собираемся переместить всю разметку в компонент. Это напрямую не повлияет на то, как работает приложение, но в дальнейшем поможет нам настроить его так, как нам нужно.
Приведём файл
app/templates/components/roll-dice.hbs
к такому состоянию:<form class="col s12">
<div class="row">
<div class="input-field col s12">
<input placeholder="Name" id="roll_name" type="text" class="validate">
<label for="roll_name">Name of Roll</label>
</div>
</div>
<div class="row">
<div class="input-field col s6">
<input placeholder="Number of dice" id="number_of_dice" type="number" class="validate" value="1">
<label for="number_of_dice">Number of Dice</label>
</div>
<div class="input-field col s6">
<input placeholder="Number of sides" id="number_of_sides" type="number" class="validate" value="6">
<label for="number_of_sides">Number of Sides</label>
</div>
</div>
<div class="row">
<button class="btn waves-effect waves-light" type="submit" name="action">
Roll Dice
<i class="material-icons right">send</i>
</button>
</div>
</form>
Теперь поместим следующий код в файл
app/templates/roll.hbs
:<div class="row">
{{roll-dice}}
</div>
{{outlet}}
В шаблон компонента попала та же самая разметка, которая раньше располагалась в файле шаблона маршрута. Файл шаблона маршрута при этом стал значительно проще. Тег
roll-dice
указывает Ember место, в которое нужно вывести компонент.Если посмотреть сейчас на то, как приложение выглядит в браузере, никакой разницы мы не увидим. Однако, поменялось устройство кода, он стал модульным. Мы собираемся воспользоваться только что созданным компонентом, для того, чтобы исправить неправильную вёрстку страницы и добавить нашему приложению некоторые новые возможности.
Жизненный цикл компонента
Компоненты в Ember отличаются особым жизненным циклом, при этом предусмотрено наличие хуков, вызываемых на различных стадиях их жизненного цикла. Мы собираемся, для того, чтобы дать возможность Materialize правильно вывести подписи, воспользоваться хуком
didRender
, который вызывается после вывода компонента на экран. Причём, делается это как при первом показе компонента, так и при последующих его показах.Для того, чтобы это сделать, отредактируем код компонента, который можно найти в файле
app/components/roll-dice.js
:/* global Materialize:false */
import Ember from 'ember';
export default Ember.Component.extend({
didRender() {
Materialize.updateTextFields();
}
});
Теперь каждый раз, когда мы посещаем маршрут
/roll
, используя прямую ссылку или ссылку из панели навигации, выполняется этот код, и Materialize правильно выводит подписи текстовых полей.Привязка данных
Нам хотелось бы, используя компонент, загружать данные в пользовательский интерфейс и выгружать их из него. Сделать это очень просто, но, что интересно, в руководстве по Ember об этом ничего нет. В результате процедуры привязки данных в Ember выглядят сложнее, чем есть на самом деле.
Каждый фрагмент данных, с которым мы хотим работать, существует в виде поля класса
Component
. Зная это, мы можем использовать вспомогательные средства, которые позволяют отображать поля ввода для компонента и привязывать эти поля к переменным компонента. В результате можно взаимодействовать с данными напрямую, не задумываясь о работе с DOM.В данном случае имеется три поля, поэтому нужно добавить в
app/components/roll-dice.js
, внутри блока определения компонента, три строки кода: rollName: '',
numberOfDice: 1,
numberOfSides: 6,
Затем настроим шаблон на вывод этих данных, используя вспомогательные механизмы вместо обычной HTML-разметки. Для того, чтобы это сделать, нужно заменить теги
<input>
на следующий код:<div class="row">
<div class="input-field col s12">
<!-- This replaces the <input> tag for "roll_name" -->
{{input placeholder="Name" id="roll_name" class="validate" value=(mut rollName)}}
<label for="roll_name">Name of Roll</label>
</div>
</div>
<div class="row">
<div class="input-field col s6">
<!-- This replaces the <input> tag for "number_of_dice" -->
{{input placeholder="Number of dice" id="number_of_dice" type="number" class="validate" value=(mut numberOfDice)}}
<label for="number_of_dice">Number of Dice</label>
</div>
<div class="input-field col s6">
<!-- This replaces the <input> tag for "number_of_sides" -->
{{input placeholder="Number of sides" id="number_of_sides" type="number" class="validate" value=(mut numberOfSides)}}
<label for="number_of_sides">Number of Sides</label>
</div>
</div>
Обратите внимание на то, что синтаксис атрибута
value
выглядит необычно. Подобные конструкции можно использовать для любых атрибутов тега, а не только для value
. Вот три способа их использования:- В виде строки, заключённой в кавычки. При таком подходе значение выводится в том виде, в котором оно представлено в коде.
- В виде строки без кавычек. В этом случае значение берётся из соответствующего фрагмента данных компонента, но элемент, выводящий данные, не влияет на компонент.
- В виде конструкции
(mut <name>)
. Благодаря этому соответствующее значение берётся из компонента, и компонент меняется (mutate) при изменении значения в браузере.
Всё это значит, что теперь мы можем работать с тремя полями компонента так, как будто бы они являются значениями полей ввода, а всю остальную работу выполнят внутренние механизмы Ember.
Действия компонента
Теперь мы хотим наладить взаимодействие с компонентом. В частности, нам хотелось бы обрабатывать событие нажатия на кнопку
Roll Dice
. В Ember подобное реализуется с помощью действий (Actions). Это — фрагменты кода компонента, которые можно подключать к шаблону. Действия — это функции, реализующие необходимую реакцию приложения, определённые в классе компонента, внутри специального поля, которое называется actions
.Сейчас мы просто собираемся сообщить пользователю о том, какие данные он ввёл в форму, но больше делать пока ничего не будем (этим займёмся ниже). Тут будет использоваться действие самой формы
OnSubmit
. Это означает, что действие будет вызвано, если пользователь щёлкнет по кнопке или нажмёт клавишу Enter
в одном из полей ввода.Вот фрагмент кода, относящийся к действиям, расположенный в файле шаблона
app/components/roll-dice.hbs
: actions: {
triggerRoll() {
alert(`Rolling ${this.numberOfDice}D${this.numberOfSides} as "${this.rollName}"`);
return false;
}
}
Здесь мы возвращаем
false
для предотвращения всплытия событий. Это — стандартный подход, применяемый в HTML-приложениях. Именно в такой ситуации это не даёт операции отправки формы перезагрузить страницу.Можно заметить, что тут мы обращаемся к переменным, которые описали ранее и используем их для доступа к полям ввода. Работа с DOM здесь не применяется — всё, чем мы тут занимаемся — это просто взаимодействие с JS-переменными.
Теперь осталось лишь связать всё воедино. В файле шаблона нужно сообщить тегу формы о том, что ему нужно вызывать заданное действие при вызове события
onsubmit
. Это выражается в добавлении одного атрибута к тегу формы с использованием вспомогательного средства Ember для подключения формы к действию. В файле app/templates/components/roll-dice.hbs
это выглядит так:<form class="col s12" onsubmit={{action 'triggerRoll'}}>
Теперь, заполнив поля формы, мы можем щёлкнуть по кнопке и увидеть всплывающее окно, отображающее те данные, которые были введены на форме.
Управление передачей данных между клиентом и сервером
Теперь мы хотим, чтобы приложение всё-таки позволило «бросать» игральные кости. Эта операция подразумевает взаимодействие с сервером. Именно сервер ответственен за «броски» и за сохранение результатов. Вот какую последовательность действий мы собираемся реализовать:
- Пользователь задаёт параметры кости, которую он хочет «бросить».
- Пользователь нажимает на кнопку Roll Dice.
- Браузер отправляет данные, введённые пользователем, серверу.
- Сервер «бросает» кость, запоминает результат и отправляет результат клиенту.
- Браузер выводит результат «броска».
Выглядит не так уж и сложно, к тому же, Ember даёт простые и удобные средства для решения этих задач.
Ember позволяет реализовать вышеописанный сценарий с использованием встроенного хранилища (Store), заполняемого с использованием моделей (Models). Хранилище — это единственный источник истинной информации во всём приложении, а каждая модель (Model) представляет отдельный фрагмент информации, находящийся в хранилище. Модели содержат сведения о том, как сохранять свои данные на сервере, а хранилище знает как создавать модели и как с ними работать.
Передача управления от компонентов маршрутам
Работая над приложением важно поддерживать разделение сфер ответственности его подсистем. Маршруты (и контроллеры, о которых мы не говорили), должны иметь доступ к хранилищу. Модели же такого доступа иметь не должны.
Причина такого разделения ответственности заключается в том, что маршрут представляет собой определённый фрагмент функциональности приложения, в то время как компонент — это лишь описание некоего фрагмента пользовательского интерфейса. Для того, чтобы с этим работать, у компонентов есть возможность передавать сигналы сущностям, расположенным выше в иерархии объектов, о том, что произошло некое событие. Это очень похоже на то, как компоненты DOM могут отправлять сообщения компонентам Ember о том, что что-то произошло.
Для того, чтобы передать управление маршруту, сначала переместим код, который занимается выводом окна сообщения, из кода компонента в код маршрута. Для того, чтобы это сделать, нужно изменить некоторые файлы.
В файл, ответственный за логику маршрута, а именно, в
app/routes/roll.js
, нужно добавить следующий блок, выполняющий регистрацию действия, которое мы собираемся выполнить.actions: {
saveRoll: function(rollName, numberOfDice, numberOfSides) {
alert(`Rolling ${numberOfDice}D${numberOfSides} as "${rollName}"`);
}
}
В файле кода компонента,
app/components/roll-dice.js
, нам нужно вызвать обработчик действия при наступлении соответствующего события. Делается это с использованием средства sendAction
в уже существующем обработчике действия:triggerRoll() {
this.sendAction('roll', this.rollName, this.numberOfDice, this.numberOfSides);
return false;
}
И, наконец, нужно связать воедино то, что мы сделали в компоненте и в маршруте. Делается это в шаблоне маршрута —
app/templates/roll.hbs
. Тут надо изменить то, как выводится компонент:{{roll-dice roll="saveRoll" }}
Эта конструкция сообщает компоненту, что свойство
roll
связано с действием saveRoll
внутри маршрута. Это имя, roll
, затем используется в компоненте для того, чтобы указать вызывающему объекту на то, что был выполнен «бросок» кости. Это имя имеет смысл для нашего компонента, так как он знает, что оно используется для запроса «броска» кости, но связанное с ним действие не заботится ни о работе другого кода, ни о судьбе переданной информации.Проверив приложение мы, как уже бывало, не заметим разницы, но, если на данном этапе всё работает, это означает, что мы готовы продолжать.
Размещение данных в хранилище
Прежде чем мы сможем поместить данные в хранилище, нам нужно описать модель, которая будет эти данные представлять. Делается это с помощью уже знакомого нам
ember-cli
путём создания структуры в файловой системе и последующего её заполнения.Для того, чтобы создать класс модели, выполним следующую команду:
$ ember generate model roll
installing model
create app/models/roll.js
installing model-test
create tests/unit/models/roll-test.js
Затем сообщим модели об атрибутах, с которыми она должна работать. Для этого внесём изменения в файл
app/models/roll.js
:import DS from 'ember-data';
export default DS.Model.extend({
rollName: DS.attr('string'),
numberOfDice: DS.attr('number'),
numberOfSides: DS.attr('number'),
result: DS.attr('number')
});
Вызовы
DS.attr
определяют новые атрибуты заданного типа. Среди поддерживаемых типов — string
, number
, date
и boolean
, хотя можно описать и собственные типы, если в этом возникнет необходимость.Теперь мы можем всем этим воспользоваться для того, чтобы наконец «бросить» кость и сохранить результат. Делается это путём работы с хранилищем из действия, которое имеется в
app/routes/roll.js
:saveRoll: function(rollName, numberOfDice, numberOfSides) {
let result = 0;
for (let i = 0; i < numberOfDice; ++i) {
result += 1 + (parseInt(Math.random() * numberOfSides));
}
const store = this.get('store');
// Запрашиваем у хранилища экземпляр модели "roll" с данными.
const roll = store.createRecord('roll', {
rollName,
numberOfDice,
numberOfSides,
result
});
// Этой командой сообщаем модели о том, чтобы она сохранила себя на сервере.
roll.save();
}
Если всё это испытать, можно отметить, что нажатие на кнопку
Roll Dice
вызывает сетевой запрос к серверу. Запрос этот, однако, даёт сбой, так как сервера у нас пока нет, но даже то, что уже сделано — большой шаг вперёд.Этот материал посвящён Ember.js, вопросы организации серверной части приложения мы тут не обсуждаем. Если вам нужно разработать приложение на Ember.js вообще без серверной части, хранение данных можно организовать локально, например, с использованием ember-localstorage-adapter, который организует работу с данными исключительно средствами браузера. В противном случае можно просто написать подходящее серверное приложение. Если сервер и клиент будут правильно функционировать, приложение заработает.
Загрузка данных из хранилища
Теперь, когда в хранилище оказались какие-то данные, нам нужно извлечь их оттуда. В то же время, мы собираемся создать маршрут
index
— он используется для работы с домашней страницей.В Ember есть встроенный маршрут,
index
, который используется для вывода первой страницы приложения. Если файлы для этого маршрута не существуют, система не выдаст сообщение об ошибке, однако, и на экран ничего выведено не будет. Мы собираемся использовать этот маршрут для вывода истории «бросков» костей из хранилища.Так как маршрут
index
уже существует, для его подготовки нет нужды использовать ember-cli
. Достаточно просто создать необходимые файлы и система использует их для этого маршрута.Обработчик маршрута будет находиться в файле
app/routes/index.js
, в котором надо разместить следующий код:import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.get('store').findAll('roll');
}
});
Код маршрута обладает непосредственным доступом к хранилищу и может использовать метод
findAll
для загрузки данных обо всех сохранённых результатах «бросков» костей. Затем мы передаём эти данные в шаблон, используя метод model
.Шаблон маршрута index опишем в файле
app/templates/index.hbs
:<table>
<thead>
<tr>
<th>Name</th>
<th>Dice Rolled</th>
<th>Result</th>
</tr>
</thead>
<tbody>
{{#each model as |roll|}}
<tr>
<td>{{roll.rollName}}</td>
<td>{{roll.numberOfDice}}D{{roll.numberOfSides}}</td>
<td>{{roll.result}}</td>
</tr>
{{/each}}
</tbody>
</table>
{{outlet}}
Этот код обеспечивает прямой доступ к модели непосредственно из маршрута, а затем перебирает набор данных и генерирует строки таблицы. В итоге у нас получилась такая страница с результатами предыдущих «бросков»:
Итоги
К этому моменту, приложив не так уж и много усилий, мы разработали приложение, которое позволяет задавать параметры костей, «бросать» эти кости и просматривать историю бросков. В ходе работы мы пользовались такими методами, как привязка данных, запись данных в хранилище и их чтение, вывод страниц с использованием шаблонов и использование маршрутов для организации навигации. Это приложение можно создать с нуля меньше чем за час.
Использование Ember может значительно повысить эффективность разработки фронтенда. В отличие от библиотек, таких, как React, Ember даёт целостную инструментальную среду, которая предназначена для разработки полнофункционального приложения без необходимости применения каких-либо дополнительных средств. Наличие
ember-cli
и предварительной настройки проекта — это очень удобно, это упрощает процесс разработки и сокращает количество ошибок на всех этапах работы. Если добавить сюда поддержку сообщества, то окажется, что на Ember можно сделать всё, что угодно.К сожалению интеграция Ember в существующий проект может оказаться непростой или попросту невозможной задачей. Лучше всего, если Ember используется с самого начала работы над новым проектом. Кроме того, что называется, «из коробки», Ember весьма своеобразно работает с серверной частью приложения. Если существующий сервер не соответствует модели Ember, разработчик может потратить очень много времени и усилий на то, чтобы либо переработать бэкенд, либо найти (или написать) плагины, которые позволят работать с тем, что есть.
Ember — мощный фреймворк, он позволяет очень быстро создавать полнофункциональные клиентские части веб-приложений. Он накладывает множество требований к структуре кода проекта, но часто на практике эти требования оказываются не такими уж и жёсткими, как может показаться на первый взгляд, так как, в любом случае, код приходится структурировать.
Уважаемые читатели! Пользуетесь ли вы Ember.js?
Комментарии (15)
untilx
27.10.2017 13:56По какой-то причине (вероятно из-за одной из ранних версий twbs) все поголовно стали использовать тег <i> для иконок. Пожалуйста, не делайте так. Этот тег предназначен для использования тогда и только тогда, когда необходимо сделать курсивным некий кусок текста внутри абзаца (параграфа). Нет, вы, конечно, можете использовать его хоть для вёрстки таблицами, если все стили переопределить, но это нарушение семантики. Пользуйтесь <span>, например, ему там самое место.
stardust_kid
27.10.2017 14:14Ваше представление о семантике неверно.
fannisco
27.10.2017 21:31-1Напишите поподробней, почему представление untilx о семантике не верно?
Ваше суждение исходит из того, что внутри абзаца (параграфа) используются теги и ?balamyt92
28.10.2017 16:25In earlier versions of the HTML specification, the <.i.> tag was merely a presentational element used to display text in italics, much like the <.b.> tag was used to display text in bold letters. This is no longer true, as these tags now define semantics rather than typographic appearance. The <.i.> tag should represent a range of text with a different semantic meaning whose typical typographic representation is italicized. This means a browser will typically still display its contents in italic type, but is, by definition, no longer required to.
linkuntilx
30.10.2017 09:43+1Собственно, то, о чём я писал. Тег для текста, а не для иконок.
balamyt92
30.10.2017 10:01Нет. Вы писали что <.i.> это для того что бы сделать текст курсивом и только для этого. В то время как <.i.> рекомендован для использования в качестве специального выделения (например терминов, объектов) в тексте, по умолчанию являющееся курсивом, но может быть переопределено (например на красный фон). Собственно говоря по этому для вставки иконок этот тег и является лучшим компромиссом семантики/удобства и будет адекватно воспринят например читалками. Для просто курсива я бы рекомендовал <.em.> но это тоже не совсем верно, ведь для курсива(чисто визуального эффекта) надо использовать CSS.
PS. Все вышесказанное ИМХО основанное на то что я читал в спеках.untilx
30.10.2017 10:43Я не это имел в виду. Речь была не о курсиве, а об иконках.
И, кстати, <em> — это не курсив, а наклонный текст, они, вообще говоря, используют (по крайней мере, должны) разное начертание, если такое есть в шрифте. И то, и другое осуждается спецификацией, равно как и всякие <b>, <s> и иже с ними, если используется вне текстового контента.
fairwind
Броский заголовок и обычное tutorial app, подобное которому есть у любого фреймворка.
А где обещанные отличность, амбициозность и вообще, чем он лучше/хуже/не такой как React/Angular/Vue?
Собственно, это претензия к к автору, а не к переводчику. Но статью спасти могут только комментарии.
DenimTornado
Для меня основными моментами являются:
1. Адекватная и понятная шаблонизация, я долго жид вместе с twig и тут очень похоже. Ну не могу я, когда html внутри js, не говоря уже о css.
2. Достаточно прозрачна архитектура приложения. Честно говоря не знаю, что там сейчас в Angular, но тут всё чётко и понятно. Есть Route, к нему есть Model, если модель не приходит в нормальном RestAPI виде её надо поправить в Serializer'e. А если сервер не понимает заброс по RestAPI, у нас есть Adapter. К каждому Route у нас есть шаблон, а если надо переиспользуемые куски есть Components. Ну и если какая-то необычная логика, то берём Controller. А есть ещё Service, это если нам надо какой-то функционал а-ля авторизации. Ну и Helpers, это совсем маленькие кусочки кода типа компонентов, но с более простой логикой, по большому счёту это просто Singleton.