【現場完全ガイド】Laravelのリアルタイム体験――Broadcasting/WebSockets/SSE・通知・再接続・オフライン対応とアクセシブルなライブ更新
この記事で学べること(要点)
- Broadcasting(イベント→チャンネル→配信)と WebSockets/SSE の選び方
- Laravel Echo/Laravel WebSockets/Pusher の構成と、認証付きチャンネル(Private/Presence)
- Livewire/Alpine/Blade を使った最小 JavaScriptでのライブUI、フォールバック(ポーリング/SSE)設計
- アクセシブルなライブ更新(
aria-live・role="status"・フォーカス移動・トースト/バナー/バッジ) - 再接続・オフライン・レート制限・権限・監査ログ・テスト(Feature/Dusk)の実務
- チャット/通知センター/進捗ボード/ダッシュボード集計などのサンプル
想定読者(だれが得をする?)
- Laravel 初〜中級エンジニア:チャット、通知、ダッシュボードなど動く画面を安全に作りたい方
- テックリード/PM:外部サービス(Pusher)とセルフホスト(Laravel WebSockets)のコスト/運用比較をしたい方
- QA/アクセシビリティ担当:ライブ更新の読み上げ、キーボード操作、色に依存しない通知を標準化したい方
- CS/サポート:通知や進捗表示の言語設計を整えて問い合わせを減らしたい方
アクセシビリティレベル:★★★★★
aria-live/role="status"/role="alert"とフォーカス制御、色に依存しない状態表現、prefers-reduced-motion、キーボード到達性、オフライン時の明示と再試行など実装面を網羅します。
1. はじめに:リアルタイムは「便利さ」と「予期せぬ変化」の両立
リアルタイムUIは便利ですが、突然の変化はユーザーを迷わせます。
- 画面が勝手に変わるときは、短い言葉で状況を案内。
- 変化箇所へフォーカスを勝手に移動しない(操作中断のリスク)。
- 読み上げには
aria-live="polite"を基本とし、危険/重要時のみrole="alert"。 - 接続切断・再接続・遅延は静かに説明し、次の手段(再読み込み/軽量版)を提示。
Laravel はサーバイベント→Broadcasting→クライアント購読までを一貫サポートします。本記事は、安全でやさしいリアルタイム体験の設計・コード・運用をまとめます。
2. アーキテクチャ概観:イベント→チャンネル→配信→購読
- アプリイベントを発火(例:
OrderCreated)。 - Broadcast可能なイベントでチャンネルへ配信(public/private/presence)。
- WebSockets(Pusher/Laravel WebSockets)または SSE/ポーリングでクライアントが購読。
- クライアント側で UI更新(カウンタ、リスト追加、トースト表示)を実施。
選択指針:
- 双方向・大量接続・低遅延 → WebSockets。
- 単方向・配信中心・シンプル → SSE。
- 制約環境や最小構成 → ポーリング。
- モバイル/社内網などでプロキシの制約が強い場合、SSE/ポーリングの併用が安全です。
3. Broadcasting の基礎:イベントとチャンネル
3.1 ブロードキャスト可能なイベント
// app/Events/OrderCreated.php
namespace App\Events;
use App\Models\Order;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\PrivateChannel; // 認証が必要な場合
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class OrderCreated implements ShouldBroadcast
{
public function __construct(public Order $order) {}
public function broadcastOn(): Channel
{
// ログインユーザー専用の通知チャンネル例
return new PrivateChannel('users.'.$this->order->user_id);
}
public function broadcastAs(): string
{
return 'order.created';
}
public function broadcastWith(): array
{
return [
'id' => $this->order->id,
'number' => $this->order->number,
'total' => $this->order->total,
];
}
}
3.2 チャンネル認可
// routes/channels.php
Broadcast::channel('users.{id}', function ($user, $id) {
return (int) $user->id === (int) $id; // 所有者のみサブスク
});
- Private/Presence チャンネルはサーバ側で認可。
- Broadcastイベントはキュー経由で配信するとピーク時に強いです。
4. クライアント:Laravel Echo で購読
4.1 基本配線
npm i laravel-echo pusher-js
// resources/js/bootstrap.js(例)
import Echo from 'laravel-echo';
window.Pusher = require('pusher-js');
window.echo = new Echo({
broadcaster: 'pusher',
key: import.meta.env.VITE_PUSHER_KEY,
wsHost: import.meta.env.VITE_PUSHER_HOST,
wsPort: 6001,
wssPort: 6001,
forceTLS: false,
disableStats: true,
enabledTransports: ['ws', 'wss'], // フォールバックを使うなら調整
authEndpoint: '/broadcasting/auth', // Private/Presence
});
// 購読例
window.echo.private(`users.${userId}`)
.listen('.order.created', (e) => {
// UI更新
});
4.2 Laravel WebSockets(セルフホスト)
- Pusher互換のサーバを自前運用。コスト最適化とデータ所在の観点で有力。
- 小〜中規模では1台で十分。大規模は水平分割+Redisなどのスケール方針を。
5. SSE(Server-Sent Events):シンプルな一方向配信
// routes/web.php
Route::get('/stream/orders', function () {
return response()->stream(function () {
while (true) {
if ($event = App\Support\SseBuffer::next()) {
echo "data: ".json_encode($event)."\n\n";
ob_flush(); flush();
}
usleep(300000);
}
}, 200, ['Content-Type' => 'text/event-stream', 'Cache-Control' => 'no-cache']);
})->middleware('auth');
const es = new EventSource('/stream/orders');
es.onmessage = (ev) => {
const data = JSON.parse(ev.data);
// UI更新
};
es.onerror = () => { /* 接続断UIなど */ };
- HTTP/1.1 で動作しやすく、プロキシ越えが安定。
- 双方向やPresenceが不要なら十分な選択肢です。
6. アクセシブルなライブ更新:設計原則
- 見出しやフォーカスを勝手に移動しない。ユーザーの操作を中断しない。
- 重要ではない更新は
aria-live="polite"、緊急はrole="alert"。 - バッジ増分や新着件数はテキストでも提示(色だけに頼らない)。
- トーストは読み上げ優先度を調整(通常は
role="status")。 - 動きは短く控えめ、
prefers-reduced-motionを尊重。
6.1 最小トースト(読み上げ対応)
<div id="toast" role="status" aria-live="polite" class="sr-only"></div>
<script>
function announce(msg){
const t = document.getElementById('toast');
t.textContent = ''; // 同文再読上げ対策
setTimeout(()=> t.textContent = msg, 50);
}
</script>
6.2 新着バッジ
<button class="relative" aria-describedby="notify-help" id="bell">
<span aria-hidden="true">🔔</span>
<span id="badge" class="absolute -top-1 -right-1 rounded-full bg-red-600 text-white text-xs px-1">0</span>
</button>
<p id="notify-help" class="sr-only">新着通知の件数を表示します。</p>
<script>
let count = 0;
function onOrderCreated(){
count++;
document.getElementById('badge').textContent = count;
announce(`新しい注文が${count}件あります`);
}
</script>
- 視覚の色(赤バッジ)に加え数値と読み上げで周知。
- クリックで一覧へ移動する場合は、次のフォーカス先を設計。
7. 典型UI:チャットと通知センター
7.1 チャット(Presence チャンネル)
// routes/channels.php
Broadcast::channel('rooms.{roomId}', function ($user, $roomId) {
return $user->can('join', Room::findOrFail($roomId)) ? ['id'=>$user->id,'name'=>$user->name] : false;
});
window.echo.join(`rooms.${roomId}`) // presence
.here(users => renderUsers(users))
.joining(user => addUser(user))
.leaving(user => removeUser(user))
.listen('.message.posted', (e) => appendMessage(e));
// メッセージ送信は通常のPOST→サーバでイベント発火
アクセシビリティ
- 新着メッセージは
role="status"の領域に短文で告げる。「山田さんからメッセージ」。 - メッセージリストは
role="log"にすると追記に強い読み上げ挙動になります。 - ユーザー入退室は音でなく文言で。過度な通知は抑制。
7.2 通知センター
- 重要度低:数字バッジ+一覧で提示、既読でバッジ減。
- 重要度高:
role="alert"トースト+詳細へキーボード先導。 - 通知の有効期限・重複抑制(同一キーは上書き)を運用に組み込みます。
8. 再接続・オフライン・フォールバック
8.1 状態バナー
<div id="conn" role="status" aria-live="polite" class="text-sm text-gray-600">
接続中...
</div>
<script>
function setConn(text, cls=''){ const el = document.getElementById('conn'); el.textContent=text; el.className=cls; }
window.addEventListener('offline', ()=> setConn('オフラインです。変更は一時保存されます。','text-red-700'));
window.addEventListener('online', ()=> setConn('オンラインに復帰しました。同期します。','text-green-700'));
</script>
8.2 Echo の再接続ハンドリング
window.echo.connector.pusher.connection.bind('state_change', (states) => {
if (states.current === 'connecting') setConn('接続中...');
if (states.current === 'unavailable' || states.current === 'disconnected') setConn('接続が不安定です。自動再接続します。','text-red-700');
if (states.current === 'connected') setConn('接続中','text-green-700');
});
8.3 フォールバック方針
- 1st: WebSockets → 2nd: SSE → 3rd: ポーリング(15–60秒)。
- 重要画面のみライブ、他は明示ボタンで更新(「最新の情報を取得」)。
- ライブ不可でも機能が失われない設計優先。
9. セキュリティ・権限・レート制限・監査
- Private/Presence チャンネルはサーバで厳格認可。
- イベントのペイロード最小化(機微情報は含めない)。
- メッセージ投稿や通知作成はRate Limitingでスパム/濫用対策。
- 監査ログに「誰が/いつ/どのチャンネルへ/どの種類のイベント」を記録。
- 接続IDやユーザーIDでトレースできるよう構造化ログ化。
10. サーバ運用:Horizon/Queues・WebSockets・スケール
- Broadcasting はキュー化してピークを平準化。
- Laravel WebSockets を使うなら監視パネルで接続数/メッセージ量を把握。
- 多接続ではヘルスチェックとオートスケール、CDN/エッジで静的配信を分離。
- 逆プロキシ(Nginx)のタイムアウト/ヘッダを最適化(SSEは
X-Accel-Buffering: no等)。
11. 進捗とジョブのライブ表示(ダッシュボード)
11.1 進捗イベント
class ExportProgress implements ShouldBroadcast {
public function __construct(public int $userId, public int $percent) {}
public function broadcastOn(){ return new PrivateChannel("users.{$this->userId}"); }
public function broadcastAs(){ return 'export.progress'; }
}
window.echo.private(`users.${userId}`)
.listen('.export.progress', e => updateProgress(e.percent));
11.2 進捗UI(アクセシブル)
<div aria-labelledby="exp-title">
<h2 id="exp-title">エクスポートの進行状況</h2>
<div role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" id="bar"
aria-describedby="exp-help">0%</div>
<p id="exp-help" class="sr-only">完了するとダウンロードリンクが表示されます。</p>
</div>
<script>
function updateProgress(p){
const bar = document.getElementById('bar');
bar.setAttribute('aria-valuenow', p);
bar.textContent = `${p}%`;
if (p===100) announce('エクスポートが完了しました。');
}
</script>
- 進捗は数値で表示し、色の変化に依存しない。
- 完了時は
role="status"で短く通知。
12. テスト:Feature・Dusk・スモークa11y
12.1 Feature(イベント発火)
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\Facades\Event;
test('OrderCreated broadcast payload', function () {
Event::fake();
$order = Order::factory()->create();
event(new \App\Events\OrderCreated($order));
Event::assertDispatched(\App\Events\OrderCreated::class);
});
12.2 Dusk(UI反映と読み上げ)
public function test_toast_announces_on_message()
{
$this->browse(function (Browser $b) {
$b->visit('/dashboard')
->script("announce('新しい注文があります')"); // 疑似注入
$b->assertSeeIn('#toast','新しい注文があります');
});
}
12.3 a11yスモーク(Pa11y/axe)
- トースト領域に
role="status"が存在。 - 重要通知画面に
role="alert"が重複・乱用されていない。 - バッジのみで意味が伝わらない UI がない。
13. 実装サンプル(最小・通知カウンタ)
13.1 ルート/イベント
// routes/channels.php
Broadcast::channel('users.{id}', fn($user,$id) => (int)$user->id === (int)$id);
// app/Events/NotifyUser.php
class NotifyUser implements ShouldBroadcast {
public function __construct(public int $userId, public string $message){}
public function broadcastOn(){ return new PrivateChannel("users.{$this->userId}"); }
public function broadcastAs(){ return 'notify.user'; }
public function broadcastWith(){ return ['message'=>$this->message]; }
}
13.2 Blade
<button id="bell" aria-describedby="notify-help" class="relative">
<span aria-hidden="true">🔔</span>
<span id="badge" class="absolute -top-1 -right-1 bg-red-600 text-white text-xs rounded px-1">0</span>
</button>
<p id="notify-help" class="sr-only">新着通知の件数を表示します。</p>
<div id="toast" role="status" aria-live="polite" class="sr-only"></div>
<script type="module">
import Echo from 'laravel-echo';
window.Pusher = (await import('pusher-js')).default;
const echo = new Echo({ broadcaster:'pusher', key:import.meta.env.VITE_PUSHER_KEY, wsHost:location.hostname, wsPort:6001, forceTLS:false, disableStats:true });
let count = 0;
echo.private(`users.${@js(auth()->id())}`).listen('.notify.user', (e) => {
count++;
document.getElementById('badge').textContent = count;
announce(e.message);
});
</script>
14. よくある落とし穴と回避策
- 重要でない更新まで
alert乱用 → 通常はstatus/polite。 - ライブ更新でフォーカスを強制移動 → しない。ユーザー操作を最優先。
- 失敗や切断が無言 → 接続状態を短文で明示、再試行を提示。
- Private 認可が甘い →
routes/channels.phpで厳格に。 - イベントペイロードが過大 → 必要最小+IDで再取得。
- フォールバック不在 → SSE/ポーリングを併用、明示更新ボタンも併置。
- 色だけのバッジ → 数値/文言を併記。
- 動き過多 →
prefers-reduced-motionを尊重し、短く控えめに。
15. チェックリスト(配布用)
アーキテクチャ
- [ ] イベントは
ShouldBroadcast、ペイロード最小 - [ ] Private/Presence の認可を実装
- [ ] キュー経由で配信、Horizon監視
クライアント
- [ ] Echo 配線、再接続メッセージ
- [ ] SSE/ポーリングのフォールバック
- [ ] 接続状態の表示(
role="status")
アクセシビリティ
- [ ] 通知は
role="status" aria-live="polite"、緊急のみalert - [ ] バッジは数値/テキスト併記、色依存なし
- [ ] フォーカス移動の抑制、操作中断を避ける
- [ ]
prefers-reduced-motionの尊重
セキュリティ/運用
- [ ] routes/channels で厳格認可
- [ ] Rate Limiting/監査ログ
- [ ] 断/復帰時の案内、再試行導線
- [ ] 監視(接続数/遅延/エラー率)
テスト
- [ ] Feature:イベント発火/認可
- [ ] Dusk:トースト/バッジ/読み上げ
- [ ] a11y:
status/alertの適切性
16. まとめ
- Laravel Broadcasting と Echo で堅実なリアルタイム基盤を構築。
- Private/Presence 認可と最小ペイロードで安全に。
- UI は読み上げに優しい通知と色に依存しない状態表示を基本に。
- 接続断/復帰/再接続は短い言葉で案内し、フォールバックを用意。
- テストと監視を回し、静かに・確実に・やさしく動く体験を保守しましょう。
