Передача данных клиентам, используя протокол Mercure
Дата обновления перевода 2024-07-31
Передача данных клиентам, используя протокол Mercure
Трансляция данных с серверов клиентам в реальном времени является требованием для множества совеременных веб и мобильных приложений.
Создание UI, реагирующего в режиме живого времени на изменения, сделанные другими пользователями (например, пользователь изменяет данные, которые в текущий момент просматривают несколько других пользователей, и все UI автоматически обновляются), уведомляющего пользователя, когда была выполнена асинхронная работа, или создание чат-приложений - наиболее распространенные случаи применения, требующие "пуш" возможностей.
Symfony предоставляет доступный компонент, который строится над протоколом Mercure, специально спроектированным для этого класса случаев применения.
Mercure - это открытый протокол, созданный с нуля, для публикации обновлений с сервера клиентам. Это современная и эффективная альтернатива поллингу, основанному на таймере, и WebSocket.
Так как он строится поверх Событий, отправленных сервером (SSE), Mercure поддерживается сразу после установки в большинстве современных браузеров (Edge и IE требуют polyfill), и имеет реализации на высоком уровне во многих языках программирования.
Mercure поставляется с механизмом авторизации, автоматическим переподключением в случае проблем сети с извлечением утерянных обновлений, наличием API, пуш-сообщения "без-соединения" для смартфонов и автоматическим обнаружением (поддерживаемый клиент может автоматически обнаружить и подписаться на обновления заданного ресурса, благодаря специфическому HTTP-заголовку).
Все эти функции поддерживаются в интеграции Symfony.
В этой записи вы можете увидеть, как веб-API Symfony использует Mercure и платформу API, чтобы делать обновления в прямом эфире в приложении React и мобильном приложении (React Native), которые генерируются с использованием генератора клиентов платформы API.
Установка
Установка пакета Symfony
Выполните эту команду, чтобы установить поддержку Mercure:
1
$ composer require mercure
Чтобы управлять стойкими соединениями, Mercure полагается на Хаб: специальный сервер, который обрабатывает стойкие соединения SSE с клиентами. Приложение Symfony публикует обновления на хабе, который распространит их по клиентам.
Благодаря интеграции Docker с Symfony, Flex
предлагает установить хаб Mercure. Выполните docker-compose up
, чтобы запустить
хаб, если вы выбрали эту опцию.
Если вы используете локальный веб-сервер Symfony,
вы должны начать его с опции --no-tls
.
1
$ symfony server:start --no-tls -d
Запуск хаба Mercure
Если вы используете интеграцию Docker, хаб уже запущен и работает, и вы можете перейти прямиком к следующему разделу.
В других случаях, и в прозиводстве, вам нужно установить хаб самостоятельно.
Хаб официального и открытого источника (AGPL), основанный на веб-сервере Caddy,
можно скачать как статичную бинарность с Mercure.rocks.
Изображение Docker, схема Helm для Kubernetes и управляемый Хаб высокой
доступности, также предоставляются.
Конфигурация
Предпочитаемый способ конфигурирования MercureBundle - с использованием переменных окружения.
Когда MercureBundle будет установлен, файл .env
вашего проекта, будет обновлен
рецептом Flex, чтобы включать в себя доступные переменные окружения.
Также, если вы используете интеграцию Docker с локальным веб-сервером Symfony, Symfony Docker или дистрибуцию платформы API, правильные переменные окружения были установлены автоматически. Прямиком переходите к следующему разделу.
В других случаях, установите URL вашего хаба как значения переменных окружения
MERCURE_URL
и MERCURE_PUBLIC_URL
.
Иногда приложению Symfony может понадобиться вызвать другой URL (обычно для публикации)
и другого клиента JavaScript. Это особенно распространено, когда приложение Symfony должно
использовать локальный URL, а код JavaScript клиентской стороны - публичный. В таком случае,
MERCURE_URL
должен содержать локальный URL, который будет использован приложением
Symfony (например, https://mercure/.well-known/mercure
), а MERCURE_PUBLIC_URL
-
публичнодоступным URL (например, https://example.com/.well-known/mercure
).
Клиенты также должны иметь веб-токен JSON (JWT), чтобы хаб Mercure был авторизован для публикации обновлений, и, иногда, подписок.
Этот токен должен быть подписан тем же секретным ключом, что был использован Хабом, для
верификации JWT (!ChangeThisMercureHubJWTSecretKey!
, если вы используете интеграцию Docker).
Этот секретный ключ должен храниться в переменной окружения MERCURE_JWT_SECRET
. MercureBundle
будеи использовать его, чтобы автоматически сгенерировать и подписать необходимые JWT.
В дополнение к этим переменным окружения, MercureBundle предоставляет более продвинутую конфигурацию:
secret
: Ключ, используемый для подписания JWT - Ключ того же размера, что и хешированный вывод (например, 256 битов для "HS256") или больше ДОЛЖНО быть использовано. (Все другие опции, кромеalgorithm
,subscribe
, иpublish
, будут проигнорированы)publish
: Список тем, разрешённых к публикации, при генерировании JWT (используется только когда предоставленыsecret
илиfactory
)subscribe
: Список тем, на которые можно подписываться при генерировании JWT (используется только когда предоставленыsecret
илиfactory
)algorithm
: Алгоритм, используемые для подписания JWT (используется только когда предоставленsecret
)provider
: ID сервиса, который надо вызвать, чтобы предоставить JWT (все другие опции будут проигнорированы)factory
: ID сервиса, который надо вызвать, чтобы создать JWT (все другие опции, кромеsubscribe
иpublish
, будут проигнорированы)value
: Сырой JWT для использования (все другие опции будут проигнорированы)
1 2 3 4 5 6 7 8 9 10 11 12 13
# config/packages/mercure.yaml
mercure:
hubs:
default:
url: https://mercure-hub.example.com/.well-known/mercure
jwt:
secret: '!ChangeThisMercureHubJWTSecretKey!'
publish: ['foo', 'https://example.com/foo']
subscribe: ['bar', 'https://example.com/bar']
algorithm: 'hmac.sha256'
provider: 'My\Provider'
factory: 'My\Factory'
value: 'my.jwt'
Tip
Полезная нагрузка JWT должна содержать как минимум следующую структуру для того, чтобы клиентам было разрешено публиковать:
1 2 3 4 5
{
"mercure": {
"publish": ["*"]
}
}
Сайт jwt.io - это удобный способ создания и подписания JWT, посмотрите на этот пример JWT. Не забудьте правильно задать секретный ключ в нижней части правой панели формы!
Базовое использование
Публикация
Компонент Mercure предоставляет объект значения Update
, представляющий собой
обновление для публикации. Он также предоставляет сервис Publisher
для запуска
обновлений в хабе.
Сервис Publisher
может быть внедрен, используя автомонтирования
в любой другой сервис, включая контроллеры:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// src/Controller/PublishController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
class PublishController extends AbstractController
{
public function publish(HubInterface $hub): Response
{
$update = new Update(
'https://example.com/books/1',
json_encode(['status' => 'OutOfStock'])
);
$hub->publish($update);
return new Response('published!');
}
}
Первым параметром для передачи конструктору Update
, является обновляемая
тема. Эта тема должна быть IRI (Интернационализированный идентификатор ресурса,
RFC 3987): уникальный идентификатор запускаемого ресурса.
Обычно, этот параметр содержит изначальный URL ресурса, переданного клиенту, но он может быть любой строкой или IRI, и не должен быть существующим URL (схоже с пространствами имен XML).
Второй параметр конструктора - содержание обновления. Это может быть что угодно, хранимое в любом формате. Однако, сериализация ресурса в формате гипермедиа, вроде JSON-LD, Atom, HTML или XML является рекомендуемой.
Подписки
Подписка на обновления JavaScript в шаблоне Twig является однозначной:
1 2 3 4 5 6 7
<script>
const eventSource = new EventSource("{{ mercure('https://example.com/books/1')|escape('js') }}");
eventSource.onmessage = event => {
// Будет вызван каждый раз, когда сервер публикует обновление
console.log(JSON.parse(event.data));
}
</script>
Функция Twig mercure()
сгенерирует URL хаба Mercure в соответствии с конфигурацией.
URL будет включать в себя параметры запроса topic
, соответствующие темам, переданным
в качестве первого аргумента.
Если вы хотите получить доступ к этому URL из внешнего файла JavaScript, сгенерируйте URL в соответствующему HTML-элементе:
1 2 3
<script type="application/json" id="mercure-url">
{{ mercure('https://example.com/books/1')|json_encode(constant('JSON_UNESCAPED_SLASHES') b-or constant('JSON_HEX_TAG'))|raw }}
</script>
Затем извлеките его из своего файла JS:
1 2 3
const url = JSON.parse(document.getElementById("mercure-url").textContent);
const eventSource = new EventSource(url);
// ...
Mercure также позволяет подписываться на несколько тем, и использовать
Шаблоны URI или специальное значение *
(соответствующее всем темам), в
качестве паттернов:
1 2 3 4 5 6 7 8 9 10 11 12
<script>
{# Подпишитесь на обновления нескольких источников Книг и на все источники Обзоров, совпадающие с заданным паттерном #}
const eventSource = new EventSource("{{ mercure([
'https://example.com/books/1',
'https://example.com/books/2',
'https://example.com/reviews/{id}'
])|escape('js') }}");
eventSource.onmessage = event => {
console.log(JSON.parse(event.data));
}
</script>
Tip
Google Chrome DevTools нативно интегрируют практичнsq UI, отображающий полученные события в реальном времени:
Чтобы использовать его:
- откройте DevTools
- выберите вкладку "Network"
- нажмите на запрос к хабу Mercure
- нажмите на подвкладку "EventStream".
Tip
Протестируйте, совпадает ли Шаблон URI с URL, используя онлайн-отладчик
Обнаружение
Протокол Mercure имеет механизм обнаружения. Для его использования,
приложение Symfony должно показать URL хаба Mercure в HTTP-заголовке
Link
.
Вы можете создавать заголовки Link
с помощью класса-помощника Discovery
(за кулисами он использует Компонент WebLink):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// src/Controller/DiscoverController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mercure\Discovery;
class DiscoverController extends AbstractController
{
public function discover(Request $request, Discovery $discovery): JsonResponse
{
// Ссылка: <https://hub.example.com/.well-known/mercure>; rel="mercure"
$discovery->addLink($request);
return $this->json([
'@id' => '/books/1',
'availability' => 'https://schema.org/InStock',
]);
}
}
Затем, этот заголовок может быть проанализирован с клиентской стороны, чтобы обнаружить URL хаба, и подписаться на него:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// Извлеките изначальный источник, обслуживаемый веб-API Symfony
fetch('/books/1') // Имеет ссылку: <http://localhost:3000/.well-known/mercure>; rel="mercure"
.then(response => {
// Извлеките URL хаба из заголовка Link
const hubUrl = response.headers.get('Link').match(/<([^>]+)>;\s+rel=(?:mercure|"[^"]*mercure[^"]*")/)[1];
// Добавьте в начало тему(ы) для подписки в качестве параметра запроса
const hub = new URL(hubUrl);
hub.searchParams.append('topic', 'http://example.com/books/{id}');
// Подпишитесь на обновления
const eventSource = new EventSource(hub);
eventSource.onmessage = event => console.log(event.data);
});
Авторизация
Mercure также позволяет запускать обновления только для авторизованных
клиентов. Чтобы сделать это, отметьте обновление как приватное, установив
третий параметр конструктора Update
как true
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// src/Controller/Publish.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mercure\Update;
class PublishController extends AbstractController
{
public function publish(HubInterface $hub): Response
{
$update = new Update(
'https://example.com/books/1',
json_encode(['status' => 'OutOfStock']),
true // private
);
// JWT издателя должен содержать эту тему, совпадающий с ней шаблон URI или * в mercure.publish, иначе вы получите ошибку 401
// JWT подписчика должен содержать эту тему, совпадающий с ней шаблон URI или * в mercure.subscribe, чтобы получить обновление
$hub->publish($update);
return new Response('private update published!');
}
}
Для того, чтобы подписаться на приватные обновления, подписчики должны предоставить хабу JWT, содержащий выборщик темы, совпадающий с темой обновления.
Чтобы предоставить этот JWT, подписчик может использовать куки, или HTTP-заголовок
Authorization
.
Куки могут быть установлены Symfony автоматически, путем передачи соответствующих
опций функции Twig mercure()
. Куки, установленные Symfony, будут автоматически
переданы браузерами хабу Mercure, если атрибут withCredentials
класса EventSource
установлен как true
. Затем, Хаб верифицирует валидность предоставленного JWT, и
излечет из него селекторы тем.
1 2 3 4 5
<script>
const eventSource = new EventSource("{{ mercure('https://example.com/books/1', { subscribe: 'https://example.com/books/1' })|escape('js') }}", {
withCredentials: true
});
</script>
Поддерживаемые опции:
subscribe
: список селекторов тем для включения в заявление JWTmercure.subscribe
publish
: список селекторов тем для включения в заявление JWTmercure.publish
additionalClaims
: дополнительные заявления для включения в JWT (дата истечения срока годности, ID токена...)
Использование куки - наиболее защищенный и предпочитаемый путь, когда клиент является веб-браузером. Если клиент - не веб-браузер, лучше пойти путем использования заголовка авторизации.
Caution
Чтобы использовать метод аутентификации куки, приложение Symfony и Хаб должны быть поданы с одного домена (могут быть разные под-домены).
Tip
Нативная реализация EventSource не позволяет указывать заголовки. Например, авторизацию, использующую токен Bearer. Чтобы достичь этого, используйте полизаполнение
1 2 3 4 5 6 7
<script>
const es = new EventSourcePolyfill("{{ mercure('https://example.com/books/1') }}", {
headers: {
'Authorization': 'Bearer ' + token,
}
});
</script>
Программная настройка куки
Иногда, может быть удобно установить куки авторизации из вашего кода, а не использовать
функцию Twig. MercureBundle предоставляет удобный сервис,
Authorization
, чтобы сделать это.
В следующем примере контроллера, добавленный куки содержит JWT, который сам содержит соответствующий селектор темы.
А вот и контроллер:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// src/Controller/DiscoverController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mercure\Authorization;
use Symfony\Component\Mercure\Discovery;
class DiscoverController extends AbstractController
{
public function publish(Request $request, Discovery $discovery, Authorization $authorization): JsonResponse
{
$discovery->addLink($request);
$authorization->setCookie($request, ['https://example.com/books/1']);
return $this->json([
'@id' => '/demo/books/1',
'availability' => 'https://schema.org/InStock'
]);
}
}
Tip
Вы не можете использовать помощник mercure()
и метод setCookie()
одновременно
(это установит куки дважды в одном запросе). Выберите один из методов.
Программное генерирование JWT, используемых для публикации
Вместо хранения JWT напрямую в конфигурации, вы можете создать
поставщик токенов, который будет возвращать токен, используемый
объектом HubInterface
:
1 2 3 4 5 6 7 8 9 10 11 12
// src/Mercure/MyTokenProvider.php
namespace App\Mercure;
use Symfony\Component\Mercure\Jwt\TokenProviderInterface;
final class MyTokenProvider implements TokenProviderInterface
{
public function getJwt(): string
{
return 'the-JWT';
}
}
Затем, сошлитесь на этот сервис в конфигурации пакета:
1 2 3 4 5 6 7
# config/packages/mercure.yaml
mercure:
hubs:
default:
url: https://mercure-hub.example.com/.well-known/mercure
jwt:
provider: App\Mercure\MyTokenProvider
Этот метод особенно удобен при использовании токенов, которые имеют срок окончания действия, который может быть программно обновлен.
Веб-API
При создании веб-API, удобно, когда есть возможность незамедлительно отправлять новые версии ресурсов всем подсоединенным устройствам, и обновлять их просмотр.
Платформа API может использовать компонент Mercure для автоматического запуска обновлений, каждый раз при создании, изменении или удалении API-ресурса.
Начните с установки библиотеки, используя ее официальный рецепт:
1
$ composer require api
Затем, создания следующей сущности достаточно для того, чтобы получить полноценный гипермедиа API и автоматическую трансляцию обновлений через хаб Mercure:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// src/Entity/Book.php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
#[ApiResource(mercure: true)]
#[ORM\Entity]
class Book
{
#[ORM\Id]
#[ORM\Column]
public string $name = '';
#[ORM\Column]
public string $status = '';
}
Как показано в этой записи, генератор клиентов платформы API также позволяет автоматически генерировать код полных приложений React и React Native из этого API. Эти приложения будут отображать содержания обновлений Mercure в режиме реального времени.
Прочтите документацию платформы API, чтобы узнать больше о ее поддержке Mercure.
Тестирование
Во время модульного тестирования обновления Mercure отправлять не надо.
Вместо этого вы можете использовать MockHub
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// tests/FunctionalTest.php
namespace App\Tests\Unit\Controller;
use App\Controller\MessageController;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\JWT\StaticTokenProvider;
use Symfony\Component\Mercure\MockHub;
use Symfony\Component\Mercure\Update;
class MessageControllerTest extends TestCase
{
public function testPublishing(): void
{
$hub = new MockHub('https://internal/.well-known/mercure', new StaticTokenProvider('foo'), function(Update $update): string {
// $this->assertTrue($update->isPrivate());
return 'id';
});
$controller = new MessageController($hub);
// ...
}
}
Во время функционального тестирования вы можете декорировать хаб:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// tests/Functional/Stub/HubStub.php
namespace App\Tests\Functional\Stub;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
class HubStub implements HubInterface
{
public function publish(Update $update): string
{
return 'id';
}
// реализуйте остальные методы HubInterface здесь
}
HubStub декорирует сервис хаба по умолчанию, поэтому никакие обновления на самом деле не отправляются. Вот реализация HubStub:
1 2 3
# config/services_test.yaml
App\Tests\Functional\Fixtures\HubStub:
decorates: mercure.hub.default
Так как MercureBundle поддерживает несколько автобусов, вам может понадобиться заменить другие определения сервисов, соответственно.
Tip
Symfony Panther имеет функцию для тестирования приложений с использованием Mercure.
Отладка
0.2
Панель WebProfiler была представлена в MercureBundle 0.2.
Подключите панель в вашей конфигурации следующим образом:
MercureBundle поставляется с панелью отладки. Установите пакет Debug, чтобы подключить ее:
1
$ composer require --dev symfony/debug-pack
Асинхронное развёртывание
Tip
Асинхронное развёртывание не поощряется. Большинство хабов Mercure уже обрабатывают публикации асинхронно, и использование Messenger обычно не требуется.
Вместо вызова сервиса Publisher
напрямую, вы можете также позволить Symfony
запускать обновления асинхронно, благодаря предоставленной интеграции с компонентом
Messenger.
Для начала, убедитесь, что установили компонент Messenger и правильно сконфигурировали транспорт (если вы этого не сделаете, обработчик будет вызван асинхронно).
Затем, запустите Mercure Update
в автобуме сообщений Messenger, он
будет обработан автоматически:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
// src/Controller/PublishController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mercure\Update;
use Symfony\Component\Messenger\MessageBusInterface;
class PublishController extends AbstractController
{
public function publish(MessageBusInterface $bus): Response
{
$update = new Update(
'https://example.com/books/1',
json_encode(['status' => 'OutOfStock'])
);
// Синхронно или асинхронно (Doctrine, RabbitMQ, Kafka...)
$bus->dispatch($update);
return new Response('published!');
}
}
Двигаемся дальше
- Протокол Mercure также поддерживается компонентом Notifier. Используйте его для отправки пуш-уведомлений веб-браузерам.
- Symfony UX Turbo - это библиотека, использующая Mercure для предоставления такого же опыта, как и с одностраничными приложениями, но без написания единой строчки JavaScript!