Передача даних клієнтам з використанням протоколу Mercure
Дата оновлення перекладу 2024-06-092
Передача даних клієнтам з використанням протоколу 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 публікує оновлення в хаб, який транслюватиме їх клієнтам.
Завдяки інтеграції Symfony Docker, 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": []
}
}
Так як масив пустий, додаток Symfony буде авторизовано лише для публікації оновлень (див. розділ авторизація_, щоб дізнатися більше).
Вебсайт 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 (схоже на простори імен).
Другий параметр конструктора - зміст оновлення. Це може бути що завгодно, що зберігається у будь-якому форматі. Однак, сериалізація ресурсу у форматі гіпермедіа, на кшталт 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 нативно інтегрують практичний UI, що відображає отримані події у реальному часі:
Щоб використати його:
- відкрийте DevTools
- оберіть вкладку "Network"
- натисніть на запит до хабу Mercure
- натисніть на підвкладку "EventStream".
Tip
Протестуйте, чи співпадає шаблон URI з URL, використовуючи онлайн-налагоджувач
Виявлення
Протокол Mercure має механізм виявлення. Для його використання додаток Symfony повинен
показати URL хабу Mercure в HTTP-заголовку Link
.
Ви можете створити заголовки Link
за допомогою компонента WebLink,
використовуючи метод помічника AbstractController::addLink
:
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
{
// Link: <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') // Has Link: <https://hub.example.com/.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
, щоб замінити сервіс хабу за замовчуванням, щоб оновлення реально
відправлялися:
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 вже обробляють публікації асинхронно, і використання Месенджеру зазвичай не потрібно.
Замість виклику сервісу Publisher
напряму, ви можете також дозволити Symfony запускати
оновлення асинхронно, завдяки наданій інтеграції з компонентом Messenger.
Спочатку переконайтеся, що встановили компонент Messenger і правильно сконфігурували транспорт (якщо ви цього не зробите, обробник буде викликано асинхронно).
Потім запустіть Mercure Update
в автобусі повідомлень Месенджеру, він буде оброблений
автоматично:
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!