Symfony2 → Symfony2 и Ember.js
Внимание, данная запись устарела. Работоспособность модуля ember-precompile под вопросом начиная с Emberjs 1.9, в связи с переходом на handlebars 2.0.0, а с версии эмбера 1.10+ шаблоны рендерятся с помощью HTMLBars, об этом напишу в ближайшее время.
Эту тему, конечно, в двух словах не опишешь, но попробую. Что это такое и с чем его едят - рассказывать не буду, т.к. раз уж вы сюда попали, то слова эти для вас не пустой звук. Расписывать, почему именно Symfony2 и Ember.js - тоже. Просто так сложилось. Ладно, вступление окончено.
В данной записи будет затронута только серверная часть, хотя не исключено, что когда-нибудь дойдут руки и до статеек по эмберу. Собственно, Symfony-приложение предоставит яваскриптовому приложению REST API, а так же неким образом "подготовит" его и выдаст клиенту в браузер.
Начать можно с установки необходимых файлов и поможет в этом bower, пакетный менеджер яваскриптовых файлов, оптимизированный для фронтенд-разработки (не знаю, что это значит, но так написано на официальном сайте). Для Symfony2 имеется и специальный бандл, чтобы с bower-ом управляться, SpBowerBundle. Конечно, никто вас не заставляет этим пользоваться, можете и дальше искать и качать нужные библиотеки вручную, как в программистском средневековье.
Поехали.
1 2 | npm install bower -g
composer require sp/bower-bundle
|
Обновим AppKernel
1 2 3 4 5 6 7 8 9 10 11 12 | <?php
// app/AppKernel.php
// ...
public function registerBundles()
{
$bundles = array(
// ...
new Sp\BowerBundle\SpBowerBundle(),
);
// ...
}
// ...
|
и конфигурацию
1 2 3 4 | # app/config/config.yml
sp_bower:
bundles:
SuperPuperBundle: ~
|
Установим, собственно, зависимости для bower
1 2 3 4 5 6 7 8 9 10 11 12 13 | // src/Super/PuperBundle/Resources/config/bower/
{
"name": "super-puper-bundle",
"dependencies": {
"jquery": "~1.11",
"ember": "~1.7.0",
"ember-data": "1.0.0-beta.9",
"blueimp-file-upload": "9.7.0",
"moment": "2.8.3",
"es5-shim": "4.0.3",
"typeahead.js": "0.10.5"
}
}
|
Для установки вендорких файлов достаточно выполнить команду
1 | app/console sp:bower:install
|
Для того, чтобы установка или обновление библиотек происходило при запуске композера, нужно добавить ещё пару строк
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // composer.json
{
// ...
"scripts": {
"post-install-cmd": [
// ...
"Sp\\BowerBundle\\Composer\\ScriptHandler::bowerInstall"
],
"post-update-cmd": [
// ...
"Sp\\BowerBundle\\Composer\\ScriptHandler::bowerInstall"
]
}
}
|
Следующее, в чём поможет Symfony2, это собрать все файлы яваскрипта до кучи, ведь не секрет, что у одностраничных приложений получается много этих самых файлов, а загружать их все по отдельности накладно. Специально для этих целей (сборка в один файл, минимизация и т.п.) из коробки уже имеется AsseticBundle. Не буду особо про него разглагольствовать, всё довольно-таки подробно описано уже в документации. Отдельно только отмечу один фильтр, который в контексте этой заметки никак не помешает. Это фильтр ассетика emberprecompile. Суть его заключается в компиляции Handlebars-шаблонов, используемых Ember.js, в JavaScript-код. Этого, конечно, можно и не делать, т.к. эмбер производит эту компиляцию на лету, в памяти. Но зачем? Почему бы не сделать предварительную работу сразу на сервере, единоразово, зачем тратить лишние десятки миллисекунд времени клиента при каждом запросе? Электроэнергия, опять же, зря расходуется на лишние вычисления :)
Устанавливаем модуль
1 | npm install ember-precompile -g
|
Подключаем фильтр
1 2 3 4 5 6 | # app/config/config.yml
assetic:
bundles: [ SuperPuperBundle ]
filters:
emberprecompile: ~
|
Применяем
1 2 3 4 | {% javascripts filter='emberprecompile'
'@SuperPuperBundle/Resources/public/app/templates/*.hbs' %}
<script type="text/javascript" src="{{ asset_url }}"></script>
{% endjavascripts %}
|
Ну а теперь перейдём к самой главной, наверное, функции серверноего приложения, к API. Для эмбера существуют разные адаптеры для доступа к серверу, где данные хранятся, однако чаще используется REST API. В Symfony2 имеются готовые решения и на этот случай, например FOSRestBundle вместе с JMSSerializerBundle. Для каких-нибудь немаленьких проектов использование таких монструозных бандлов оправдано, чтобы избежать большой рутинной работы или обеспечить одним взмахом левой пятки возможность отдавать и JSON, и XML, и XHTML по запросу приложения-клиента. Но я напишу как можно обойтись и без них.
При использовании эмберовского DS.RESTAdapter каждая модель извлекается/сохраняется/создаётся/удаляется по определённым URL-ам с использованием специфических методов GET, PUT, POST, и DELETE, данные при этом передаются в теле запроса в виде JSON. Из коробки такая схема не работает, но есть решение (подсмотренное мной в FOSRestBundle).
Создаём EventListener, который будет просматривать объект запроса и искать в нём JSON-данные
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | <?php
namespace Super\PuperBundle\EventListener;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class JsonBodyListener
{
/**
* @param GetResponseEvent $event
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
*/
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$method = $request->getMethod();
if (!count($request->request->all())
&& in_array($method, array('POST', 'PUT', 'DELETE'))
) {
$contentType = $request->headers->get('Content-Type');
$format = null === $contentType
? $request->getRequestFormat()
: $request->getFormat($contentType);
if ($format == 'json') {
$content = $request->getContent();
if (!empty($content)) {
$data = json_decode($content, true);
if (is_array($data)) {
$request->request = new ParameterBag($data);
} else {
throw new BadRequestHttpException(
'Invalid ' . $format . ' message received'
);
}
}
}
}
}
}
|
И подключаем его
1 2 3 4 5 6 7 | # src/Super/PuperBundle/Resources/config/services.yml
services:
super_puper.json_body_listener:
class: Super\PuperBundle\EventListener\JsonBodyListener
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 10 }
|
Вот, собственно, и всё. Теперь при сохранении эмберовской модели с именем status, например, можно выбирать данные из запроса.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <?php
// src/Super/PuperBundle/Controller/MegaController.php
/**
* @Route("/status/{id}", requirements={"id": "\d+"})
* @Method("PUT")
*
* @param Request $request
* @param Status $entity
*
* @return JsonResponse
*/
public function saveStatusAction(Request $request, Status $entity)
{
$statusData = $request->request->get('status');
// ...
}
|
На сегодня хватит :)
Комментарии