使用 Mercure 協定將資料推送至用戶端
對於許多現代化的網路和行動應用程式而言,能夠從伺服器即時廣播資料到用戶端是一項必要功能。
建立即時反應其他使用者所做變更的 UI (例如,當一位使用者變更目前正被其他多位使用者瀏覽的資料時,所有 UI 都會立即更新)、在非同步工作完成時通知使用者,或是建立聊天應用程式,都是需要「推送」功能的典型用例。
Symfony 提供了一個簡單明瞭的元件,建立在 Mercure 協定之上,專門為此類用例設計。
Mercure 是一個開放協定,從底層設計用於從伺服器發布更新到用戶端。它是基於計時器的輪詢和 WebSocket 的現代化且有效率的替代方案。
由於 Mercure 是建立在伺服器發送事件 (SSE) 之上,因此在現代瀏覽器中開箱即用 (舊版本的 Edge 和 IE 需要一個 polyfill),並且在許多程式語言中都有高階實作。
Mercure 具備授權機制、在發生網路問題時自動重新連線並擷取遺失的更新、存在 API、「無連線」智慧型手機推送,以及自動探索功能 (支援的用戶端可以透過特定的 HTTP 標頭自動探索和訂閱給定資源的更新)。
所有這些功能都在 Symfony 整合中受到支援。
在這段錄影中,您可以看到 Symfony Web API 如何運用 Mercure 和 API Platform 即時更新使用 API Platform 用戶端產生器產生的 React 應用程式和行動應用程式 (React Native)。
安裝
執行 Mercure Hub
為了管理持久連線,Mercure 依賴 Hub:一個專用的伺服器,用於處理與用戶端的持久 SSE 連線。Symfony 應用程式將更新發布到 Hub,Hub 會將其廣播給用戶端。
在生產環境中,您必須自行安裝 Mercure Hub。基於 Caddy Web 伺服器的官方開放原始碼 (AGPL) Hub 可以從 Mercure.rocks 下載為靜態二進位檔案。也提供了 Docker 映像檔、Kubernetes 的 Helm Chart 以及受管理的、高可用性 Hub。
由於 Symfony 的 Docker 整合,Flex 建議安裝 Mercure Hub 以進行開發。如果您選擇此選項,請執行 docker-compose up
以啟動 Hub。
如果您使用 Symfony 本機 Web 伺服器,則必須使用 --no-tls
選項啟動它。
1
$ symfony server:start --no-tls -d
如果您使用 Docker 整合,則 Hub 已啟動並執行。
設定
設定 MercureBundle 的首選方式是使用環境變數。
安裝 MercureBundle 後,專案的 .env
檔案已由 Flex recipe 更新,以包含可用的環境變數。
此外,如果您將 Docker 整合與 Symfony 本機 Web 伺服器、Symfony Docker 或 API Platform 發行版搭配使用,則已自動設定適當的環境變數。直接跳到下一節。
否則,請將您的 Hub URL 設定為 MERCURE_URL
和 MERCURE_PUBLIC_URL
環境變數的值。有時 Symfony 應用程式 (通常用於發布) 和 JavaScript 用戶端 (通常用於訂閱) 必須呼叫不同的 URL。當 Symfony 應用程式必須使用本機 URL,而用戶端 JavaScript 程式碼必須使用公開 URL 時,這種情況尤其常見。在這種情況下,MERCURE_URL
必須包含 Symfony 應用程式使用的本機 URL (例如 https://mercure/.well-known/mercure
),而 MERCURE_PUBLIC_URL
則必須包含公開可用的 URL (例如 https://example.com/.well-known/mercure
)。
用戶端也必須攜帶 JSON Web Token (JWT) 給 Mercure Hub,才能獲得發布更新的授權,有時也需要獲得訂閱的授權。
此權杖必須使用與 Hub 用於驗證 JWT 的相同密鑰簽署 (!ChangeThisMercureHubJWTSecretKey!
如果您使用 Docker 整合)。此密鑰必須儲存在 MERCURE_JWT_SECRET
環境變數中。MercureBundle 將使用它來自動產生和簽署所需的 JWT。
除了這些環境變數之外,MercureBundle 還提供了更進階的設定
secret
:用於簽署 JWT 的密鑰 - 必須使用與雜湊輸出大小相同的密鑰 (例如,「HS256」為 256 位元) 或更大的密鑰。(除了algorithm
、subscribe
和publish
之外,所有其他選項都將被忽略)publish
:產生 JWT 時允許發布的主題清單 (僅在提供secret
或factory
時可用)subscribe
:產生 JWT 時允許訂閱的主題清單 (僅在提供secret
或factory
時可用)algorithm
:用於簽署 JWT 的演算法 (僅在提供secret
時可用)provider
:用於呼叫以提供 JWT 的服務 ID (所有其他選項都將被忽略)factory
:用於呼叫以建立 JWT 的服務 ID (除了subscribe
和publish
之外,所有其他選項都將被忽略)value
:要使用的原始 JWT (所有其他選項都將被忽略)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
# config/packages/mercure.yaml
mercure:
hubs:
default:
url: '%env(string:MERCURE_URL)%'
public_url: '%env(string:MERCURE_PUBLIC_URL)%'
jwt:
secret: '%env(string:MERCURE_JWT_SECRET)%'
publish: ['https://example.com/foo1', 'https://example.com/foo2']
subscribe: ['https://example.com/bar1', 'https://example.com/bar2']
algorithm: 'hmac.sha256'
provider: 'My\Provider'
factory: 'My\Factory'
value: 'my.jwt'
提示
JWT 酬載必須至少包含以下結構,用戶端才能被允許發布
1 2 3 4 5
{
"mercure": {
"publish": ["*"]
}
}
jwt.io 網站是建立和簽署 JWT 的便捷方式,請查看此範例 JWT。別忘了在表單右側面板的底部正確設定您的密鑰!
基本用法
發布
Mercure 元件提供了一個 Update
值物件,代表要發布的更新。它還提供了一個 Publisher
服務,用於將更新分發到 Hub。
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) 序列化資源。
訂閱
從 Twig 範本在 JavaScript 中訂閱更新非常簡單
1 2 3 4 5 6 7
<script>
const eventSource = new EventSource("{{ mercure('https://example.com/books/1')|escape('js') }}");
eventSource.onmessage = event => {
// Will be called every time an update is published by the server
console.log(JSON.parse(event.data));
}
</script>
mercure()
Twig 函式會根據設定產生 Mercure Hub 的 URL。URL 包含與作為第一個引數傳遞的主題相對應的 topic
查詢參數。
如果您想從外部 JavaScript 檔案存取此 URL,請在專用的 HTML 元素中產生 URL
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>
{# Subscribe to updates of several Book resources and to all Review resources matching the given pattern #}
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>
但是,在用戶端 (即 JavaScript 的 EventSource
) 上,沒有內建方法可以知道特定訊息來自哪個主題。如果這 (或任何其他中繼資訊) 對您很重要,則需要將其包含在訊息的資料中 (例如,透過將金鑰新增至 JSON,或將 data-*
屬性新增至 HTML)。
提示
使用線上偵錯工具測試 URI 範本是否與 URL 相符
提示
Google Chrome 具有實用的 UI 來顯示收到的事件

在 DevTools 中,選取「網路」標籤,然後按一下對 Mercure Hub 的請求,然後按一下「EventStream」子標籤。
探索
Mercure 協定隨附探索機制。為了利用它,Symfony 應用程式必須在 Link
HTTP 標頭中公開 Mercure Hub 的 URL。
您可以使用 Discovery
輔助類別建立 Link
標頭 (在底層,它使用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
{
// Link: <https://hub.example.com/.well-known/mercure>; rel="mercure"
$discovery->addLink($request);
return $this->json([
'@id' => '/books/1',
'availability' => 'https://schema.org/InStock',
]);
}
}
然後,可以在用戶端剖析此標頭,以尋找 Hub 的 URL 並訂閱它
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// Fetch the original resource served by the Symfony web API
fetch('/books/1') // Has Link: <https://hub.example.com/.well-known/mercure>; rel="mercure"
.then(response => {
// Extract the hub URL from the Link header
const hubUrl = response.headers.get('Link').match(/<([^>]+)>;\s+rel=(?:mercure|"[^"]*mercure[^"]*")/)[1];
// Append the topic(s) to subscribe as query parameter
const hub = new URL(hubUrl, window.origin);
hub.searchParams.append('topic', 'https://example.com/books/{id}');
// Subscribe to updates
const eventSource = new EventSource(hub);
eventSource.onmessage = event => console.log(event.data);
});
授權
Mercure 也允許僅將更新分發給授權的用戶端。若要執行此操作,請將 Update
建構函式的第三個參數設定為 true
,以將更新標記為private
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
);
// Publisher's JWT must contain this topic, a URI template it matches or * in mercure.publish or you'll get a 401
// Subscriber's JWT must contain this topic, a URI template it matches or * in mercure.subscribe to receive the update
$hub->publish($update);
return new Response('private update published!');
}
}
若要訂閱 private 更新,訂閱者必須向 Hub 提供包含主題選取器的 JWT,該選取器與更新的主題相符。
若要提供此 JWT,訂閱者可以使用 Cookie 或 Authorization
HTTP 標頭。
Cookie 可以由 Symfony 自動設定,方法是將適當的選項傳遞給 mercure()
Twig 函式。如果 EventSource
類別的 withCredentials
屬性設定為 true
,則由 Symfony 設定的 Cookie 會由瀏覽器自動傳遞到 Mercure Hub。然後,Hub 會驗證提供的 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...)
當用戶端是 Web 瀏覽器時,使用 Cookie 是最安全且首選的方式。如果用戶端不是 Web 瀏覽器,則使用授權標頭是可行的方法。
警告
若要使用 Cookie 驗證方法,Symfony 應用程式和 Hub 必須從相同的網域提供服務 (可以是不同的子網域)。
提示
EventSource 的原生實作不允許指定標頭。例如,使用 Bearer 權杖進行授權。為了實現這一點,請使用 polyfill
1 2 3 4 5 6 7
<script>
const es = new EventSourcePolyfill("{{ mercure('https://example.com/books/1') }}", {
headers: {
'Authorization': 'Bearer ' + token,
}
});
</script>
程式化設定 Cookie
有時,從您的程式碼設定授權 Cookie 而不是使用 Twig 函式可能會很方便。MercureBundle 提供了一個方便的服務 Authorization
來執行此操作。
在以下範例控制器中,新增的 Cookie 包含 JWT,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'
]);
}
}
提示
您不能同時使用 mercure()
輔助函式和 setCookie()
方法 (它會在單一請求中設定兩次 Cookie)。請選擇其中一種方法。
程式化產生用於發布的 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
當使用具有到期日的權杖時,此方法特別方便,這些權杖可以透過程式化方式重新整理。
Web API
在建立 Web API 時,能夠立即將資源的新版本推送到所有連線的裝置,並更新其檢視會很方便。
API Platform 可以使用 Mercure 元件自動分發更新,每次建立、修改或刪除 API 資源時都會分發更新。
首先使用其官方 recipe 安裝程式庫
1
$ composer require api
然後,建立以下實體就足以獲得功能完整的超媒體 API,以及透過 Mercure Hub 自動廣播更新
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 Platform 用戶端產生器也允許從此 API 搭建完整的 React 和 React Native 應用程式。這些應用程式將即時呈現 Mercure 更新的內容。
請查看專用的 API Platform 文件,以深入瞭解其 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);
// ...
}
}
對於功能測試,您可以改為建立 Hub 的 Stub
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';
}
// implement rest of HubInterface methods here
}
使用 HubStub
取代預設的 Hub 服務,這樣實際上不會傳送任何更新
1 2 3 4
# config/services_test.yaml
services:
mercure.hub.default:
class: App\Tests\Functional\Stub\HubStub
由於 MercureBundle 支援多個 Hub,您可能必須相應地取代其他服務定義。
提示
Symfony Panther 具有一項功能,可用於測試使用 Mercure 的應用程式。
偵錯
0.2
WebProfiler 面板是在 MercureBundle 0.2 中引入的。
MercureBundle 隨附偵錯面板。安裝 Debug 套件以啟用它
1
$ composer require --dev symfony/debug-pack

非同步分發
提示
不建議使用非同步分發。大多數 Mercure Hub 已經非同步處理發布,通常不需要使用 Messenger。
除了直接呼叫 Publisher
服務之外,您也可以透過提供的與 Messenger 元件的整合,讓 Symfony 非同步分發更新。
首先,請務必安裝 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'])
);
// Sync, or async (Doctrine, RabbitMQ, Kafka...)
$bus->dispatch($update);
return new Response('published!');
}
}
進階
- Notifier 元件也支援 Mercure 協定。使用它將推播通知傳送到 Web 瀏覽器。
- Symfony UX Turbo 是一個使用 Mercure 的程式庫,可提供與單頁應用程式相同的體驗,但無需編寫任何 JavaScript 程式碼!