Передача даних клієнтам з використанням протоколу Mercure

Дата оновлення перекладу 2022-12-15

Передача даних клієнтам з використанням протоколу 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 для використання (всі інші опції будуть проігноровані)
  • YAML
  • XML
  • PHP
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 __invoke(Request $request, Discovery $discovery): JsonResponse
    {
        // Link: <http://localhost:3000/.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: <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 __invoke(HubInterface $hub): Response
    {
        $update = new Update(
            'http://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: список селекторів тем для додавання у заяву JWT mercure.subscribe
  • publish: список селекторів тем для додавання у заяку JWT mercure.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 getToken(): string
    {
        return 'the-JWT';
    }
}

Потім вам треба послатися на цей севіс у конфігурації пакету:

  • YAML
  • XML
  • PHP
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()
    {
        $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!