Привет, Хабр! Хочу немного вклиниться в цикл статей и показать как можно простым путём сделать обновление списка комментариев в ленте в режиме реального времени. Как это происходит, например, на github
Для реализации этой задачи будем использовать сервис Post Hawk. Код доступен в соответствующей ветке.
Если за основу у вас взята ветка 5, то необходимо добавить в composer.json зависимость:
а если 5.1, то достаточно просто
и дождаться установки (обновления) пакета(-ов).
Далее устанавливаем erlang. Билды под разные системы находятся здесь, или, если есть желание, можно собрать из исходников.
Переходим в любое место файловой системы, какое вам больше нравится (можно рядом с основным проектом) и клонируем сервер и клиент
Собираем rebar:
получившийся файлик (rebar) копируем в оба репозитория. Он нам необходим для сборки проекта.
Переходим в папку с сервером, собираем его и запускаем:
Аналогично собираем и запускаем клиент:
Кофигурация бандла:
Навигация по частям руководства
Часть 1 — Конфигурация Symfony2 и шаблонов
Часть 2 — Страница с контактной информацией: валидаторы, формы и электронная почта
Часть 3 — Doctrine 2 и Фикстуры данных
Часть 4 — Модель комментариев, Репозиторий и Миграции Doctrine 2
Часть 5 — Twig расширения, Боковая панель(sidebar) и Assetic
Часть 6 — Модульное и Функциональное тестирование
Часть 2 — Страница с контактной информацией: валидаторы, формы и электронная почта
Часть 3 — Doctrine 2 и Фикстуры данных
Часть 4 — Модель комментариев, Репозиторий и Миграции Doctrine 2
Часть 5 — Twig расширения, Боковая панель(sidebar) и Assetic
Часть 6 — Модульное и Функциональное тестирование
Для реализации этой задачи будем использовать сервис Post Hawk. Код доступен в соответствующей ветке.
Часть 1. Устанавливаем зависимости (обязательная)
Если за основу у вас взята ветка 5, то необходимо добавить в composer.json зависимость:
composer require post-hawk/hawk-api-bundle ~1.0
а если 5.1, то достаточно просто
composer update
и дождаться установки (обновления) пакета(-ов).
Далее устанавливаем erlang. Билды под разные системы находятся здесь, или, если есть желание, можно собрать из исходников.
При установке в windows, не забываем ставить галочку добавить путь в PATH.
Переходим в любое место файловой системы, какое вам больше нравится (можно рядом с основным проектом) и клонируем сервер и клиент
git clone https://github.com/postHawk/hawk_client.git
git clone https://github.com/postHawk/hawk_server
Собираем rebar:
$ git clone git://github.com/rebar/rebar.git
$ cd rebar
$ ./bootstrap
получившийся файлик (rebar) копируем в оба репозитория. Он нам необходим для сборки проекта.
Проект использует 2 версию rebar. Под rebar3 пока запустить не удаётся
Переходим в папку с сервером, собираем его и запускаем:
cd hawk_server
nano src/hawk_server.app.src
#заполняем данные о пользователе
{env, [
{statistic, [{use, false}]},
user, #{
<<"login">> => <<"symblog">>,
<<"domain">> => [ %список доменов с которых будут приниматься подключения
<<"127.0.0.1:8000">>
],
<<"key">> => <<"very secret key">> %api ключ. Должен совпадать на сервере и клиенте
}}
]}
mv .erlang .erlang_
rebar get-deps compile
mv .erlang_ .erlang
erl -name 'hawk_server@127.0.0.1' -boot start_sasl -setcookie test -kernel inet_dist_listen_min 9000 inet_dist_listen_max 9005
Аналогично собираем и запускаем клиент:
cd hawk_client
nano src/hawk_client.app.src
#заполните название server_node, например, 'test_hawk_server@127.0.0.1' и api_key (должен совпадать с серверным). Сохраните файл
{env, [
{api_key, <<"very secret key">>},
{server_node, 'hawk_server@127.0.0.1'}
]}
mv .erlang .erlang_
./rebar get-deps compile
mv .erlang_ .erlang
erl -name 'hawk_client@127.0.0.1' -boot start_sasl -setcookie test -kernel inet_dist_listen_min 9000 inet_dist_listen_max 9005
Если нужно запустить процесс в фоне, просто добавьте к набору параметров опцию -detached
Для пользователей windows утилиту запуска стоит изменить с erl на werl
Часть 2. Дорабатываем блог (обязательная для ветки 5)
Кофигурация бандла://app/AppKernel.php
$bundles = array(
...
new Hawk\ApiBundle\HawkApiBundle(),
new FOS\JsRoutingBundle\FOSJsRoutingBundle(),
);
#app/config/config.yml
hawk_api:
client:
host: '%hawk_api.client.host%' #ip или домен
port: '%hawk_api.client.port%' #порт, который слушает клиент
key: '%hawk_api.client.key%'
#app/config/parameters.yml
parameters:
hawk_api.client.host: 127.0.0.1
hawk_api.client.port: 7777
hawk_api.client.key: 'very secret key'
#app/config/routing.yml
hawk:
resource: '@HawkApiBundle/Controller/'
prefix: /hawk
fos_js_routing:
resource: "@FOSJsRoutingBundle/Resources/config/routing/routing.xml"
Ставим assets:
php app/console assets:install --symlink web
Дорабатываем контроллер:
...
use Blogger\BlogBundle\Entity\Blog;
use Hawk\ApiBundle\Event\GroupMessage;
В функцию добавления комментария помещаем отправку уведомлений:
...
$em->persist($comment);
$em->flush();
$this->sendNotification($comment, $blog);
return $this->redirect($this->generateUrl('BloggerBlogBundle_blog_show', array(
...
ну и добавляем саму функцию:
Код/**
* Отправка уведомления о новом комментарии
* @param Comment $comment комментарий
* @param Blog $blog блог
*/
private function sendNotification(Comment $comment, Blog $blog)
{
//формируем тело комментария
$comment_text = $this->renderView('BloggerBlogBundle:Comment:index.html.twig', [
'comments' => [$comment]
]);
//формируем сообщение
$gMessage = new GroupMessage();
$gMessage
->setFrom('comment_demon')
->setGroups(['blog_' . $blog->getId()])
->setText(['comment' => $comment_text])
->setEvent('new_comment') //будет сгенерирован на клиенте
;
//отсылаем
$api = $this
->container
->get('event_dispatcher')
->dispatch(GroupMessage::NEW_MESSAGE, $gMessage)
->getResult() //HawkApi
;
//если возникли ошибки пишем их в лог
if($api->hasErrors()){
$logger = $this->get('logger');
$logger->error('Error sending message: ' . print_r($api->getErrors(), 1));
}
}
Немного остановлюсь на том, что здесь происходит. Отправка сообщений через сервис возможна двумя способами. Первый это как на примере выше, через систему событий symfony. Создаётся объект сообщения (простого или группового) и посылается с определённым типом события. Второй вариант — это отправка непосредственно с помощью апи, которое можно получить из контейнера:
$api = $this->get('hawk_api.api')->getApi();
$api
->registerUser($id)
->sendMessage($from, $to, $text, $event)
->execute()
->getResult('sendMessage')
;
Дорабатываем шаблоны:
В src/Blogger/BlogBundle/Resources/views/Blog/show.html.twig меняем строку
<article class="blog">
на
<article class="blog" data-blog-id="{{ blog.id }}">
, так как нам необходим будет id блога для подписки пользователя.
Подключаем скрипты:
{#src/Blogger/BlogBundle/Resources/views/layout.html.twig#}
{% block javascripts %}
{% javascripts
'@BloggerBlogBundle/Resources/public/js/*'
output='js/plugins,js'
filter='?yui_js'
%}
<script src="{{ asset_url }}"></script>
{% endjavascripts %}
{% javascripts
'../vendor/post-hawk/hawk-api/Resources/public/js/hawk_api.min.js'
%}
<script src="{{ asset_url }}"></script>
{% endjavascripts %}
<script src="{{ asset('bundles/fosjsrouting/js/router.js') }}"></script>
<script src="{{ path('fos_js_routing_js', { callback: 'fos.Router.setData' }) }}"></script>
{% endblock %}
Ну и пишем немного js
код$(document).ready(function () {
connectToHawk();
});
function connectToHawk()
{
//отправляем запрос за токеном
$.post(Routing.generate('hawk_token', {
useSessionId: 1
}), {}, function (data) {
if(data.errors === false){
//инициализируем подключение
HAWK_API.init({
user_id: data.result.id,
token: data.result.token,
url: data.result.ws,
debug: true
});
//подписываемся на новые комментарии
HAWK_API.bind_handler('new_comment', function(e, msg){
//игнорируем служебные сообщения
if(msg.from === 'hawk_client')
return;
//находим список комментариев и последний из них
//создаём объект нового
var $comments = $('.previous-comments'),
$last = $comments.find('.comment:last'),
cls = 'odd',
$comment = $(msg.text.comment)
;
//определяемся с классом комментария
if($last.size()){
cls = $last.hasClass('odd') ? 'even' : 'odd';
}
$comment
.removeClass('odd')
.addClass(cls)
.hide()
;
//показываем
$comments.append($comment);
$comment.show('normal')
});
//подписываемся на новые комментарии
HAWK_API.bind_handler('open', function(e, msg){
var id = $('.blog').data('blogId');
//добавляем пользователя в группу блога
//если её нет, то она будет создана с публичным доступом
HAWK_API.add_user_to_group(['blog_' + id]);
});
} else {
if(data.errors !== 'no_user') {
console.error(data);
}
}
});
}
Первое, что мы делаем, это отправляем запрос за токеном для подключения к серверу сообщений. Так как пользователь у нас не аторизованный, то говорим контроллеру использовать в качестве id пользователя, id его сессии.
Если всё хорошо, то мы получим в ответе: id пользователя, токен подключения и адрес сервера.
Далее, инициализируем подключение и подписываемся на события. В данном случае, нас интересуют два из них. Первое — open, возникает после успешного подключения к сокету. Второе — new_comment (его мы передаём при отправке сообщения), непосредственно новый комментарий.
Вот собственно и всё. Благодарю за внимание. Код этой части доступен на github в соответствующей ветке.
//app/AppKernel.php
$bundles = array(
...
new Hawk\ApiBundle\HawkApiBundle(),
new FOS\JsRoutingBundle\FOSJsRoutingBundle(),
);
#app/config/config.yml
hawk_api:
client:
host: '%hawk_api.client.host%' #ip или домен
port: '%hawk_api.client.port%' #порт, который слушает клиент
key: '%hawk_api.client.key%'
#app/config/parameters.yml
parameters:
hawk_api.client.host: 127.0.0.1
hawk_api.client.port: 7777
hawk_api.client.key: 'very secret key'
#app/config/routing.yml
hawk:
resource: '@HawkApiBundle/Controller/'
prefix: /hawk
fos_js_routing:
resource: "@FOSJsRoutingBundle/Resources/config/routing/routing.xml"
Ставим assets:
php app/console assets:install --symlink web
Дорабатываем контроллер:
...
use Blogger\BlogBundle\Entity\Blog;
use Hawk\ApiBundle\Event\GroupMessage;
В функцию добавления комментария помещаем отправку уведомлений:
...
$em->persist($comment);
$em->flush();
$this->sendNotification($comment, $blog);
return $this->redirect($this->generateUrl('BloggerBlogBundle_blog_show', array(
...
ну и добавляем саму функцию:
Код
/**
* Отправка уведомления о новом комментарии
* @param Comment $comment комментарий
* @param Blog $blog блог
*/
private function sendNotification(Comment $comment, Blog $blog)
{
//формируем тело комментария
$comment_text = $this->renderView('BloggerBlogBundle:Comment:index.html.twig', [
'comments' => [$comment]
]);
//формируем сообщение
$gMessage = new GroupMessage();
$gMessage
->setFrom('comment_demon')
->setGroups(['blog_' . $blog->getId()])
->setText(['comment' => $comment_text])
->setEvent('new_comment') //будет сгенерирован на клиенте
;
//отсылаем
$api = $this
->container
->get('event_dispatcher')
->dispatch(GroupMessage::NEW_MESSAGE, $gMessage)
->getResult() //HawkApi
;
//если возникли ошибки пишем их в лог
if($api->hasErrors()){
$logger = $this->get('logger');
$logger->error('Error sending message: ' . print_r($api->getErrors(), 1));
}
}
Немного остановлюсь на том, что здесь происходит. Отправка сообщений через сервис возможна двумя способами. Первый это как на примере выше, через систему событий symfony. Создаётся объект сообщения (простого или группового) и посылается с определённым типом события. Второй вариант — это отправка непосредственно с помощью апи, которое можно получить из контейнера:
$api = $this->get('hawk_api.api')->getApi();
$api
->registerUser($id)
->sendMessage($from, $to, $text, $event)
->execute()
->getResult('sendMessage')
;
Дорабатываем шаблоны:
В src/Blogger/BlogBundle/Resources/views/Blog/show.html.twig меняем строку
<article class="blog">
на
<article class="blog" data-blog-id="{{ blog.id }}">
, так как нам необходим будет id блога для подписки пользователя.
Подключаем скрипты:
{#src/Blogger/BlogBundle/Resources/views/layout.html.twig#}
{% block javascripts %}
{% javascripts
'@BloggerBlogBundle/Resources/public/js/*'
output='js/plugins,js'
filter='?yui_js'
%}
<script src="{{ asset_url }}"></script>
{% endjavascripts %}
{% javascripts
'../vendor/post-hawk/hawk-api/Resources/public/js/hawk_api.min.js'
%}
<script src="{{ asset_url }}"></script>
{% endjavascripts %}
<script src="{{ asset('bundles/fosjsrouting/js/router.js') }}"></script>
<script src="{{ path('fos_js_routing_js', { callback: 'fos.Router.setData' }) }}"></script>
{% endblock %}
Ну и пишем немного js
код
$(document).ready(function () {
connectToHawk();
});
function connectToHawk()
{
//отправляем запрос за токеном
$.post(Routing.generate('hawk_token', {
useSessionId: 1
}), {}, function (data) {
if(data.errors === false){
//инициализируем подключение
HAWK_API.init({
user_id: data.result.id,
token: data.result.token,
url: data.result.ws,
debug: true
});
//подписываемся на новые комментарии
HAWK_API.bind_handler('new_comment', function(e, msg){
//игнорируем служебные сообщения
if(msg.from === 'hawk_client')
return;
//находим список комментариев и последний из них
//создаём объект нового
var $comments = $('.previous-comments'),
$last = $comments.find('.comment:last'),
cls = 'odd',
$comment = $(msg.text.comment)
;
//определяемся с классом комментария
if($last.size()){
cls = $last.hasClass('odd') ? 'even' : 'odd';
}
$comment
.removeClass('odd')
.addClass(cls)
.hide()
;
//показываем
$comments.append($comment);
$comment.show('normal')
});
//подписываемся на новые комментарии
HAWK_API.bind_handler('open', function(e, msg){
var id = $('.blog').data('blogId');
//добавляем пользователя в группу блога
//если её нет, то она будет создана с публичным доступом
HAWK_API.add_user_to_group(['blog_' + id]);
});
} else {
if(data.errors !== 'no_user') {
console.error(data);
}
}
});
}
Первое, что мы делаем, это отправляем запрос за токеном для подключения к серверу сообщений. Так как пользователь у нас не аторизованный, то говорим контроллеру использовать в качестве id пользователя, id его сессии.
Если всё хорошо, то мы получим в ответе: id пользователя, токен подключения и адрес сервера.
Далее, инициализируем подключение и подписываемся на события. В данном случае, нас интересуют два из них. Первое — open, возникает после успешного подключения к сокету. Второе — new_comment (его мы передаём при отправке сообщения), непосредственно новый комментарий.
Вот собственно и всё. Благодарю за внимание. Код этой части доступен на github в соответствующей ветке.
Поделиться с друзьями
BoShurik
Давно уже не пользовался ассетиком, но вроде бы он умеет так (gulp так точно может):
Slavenin999
Да, действительно умеет, спасибо! Подправил статью.