php elephant sticker
Photo by RealToughCandy.com on Pexels.com
目次

[Guía Práctica de Campo] Experiencias en Tiempo Real en Laravel — Broadcasting/WebSockets/SSE, Notificaciones, Reconexiones, Soporte Offline y Actualizaciones en Vivo Accesibles

Qué aprenderás (puntos destacados)

  • Cómo elegir entre Broadcasting (evento → canal → entrega) y WebSockets / SSE
  • Arquitectura para Laravel Echo / Laravel WebSockets / Pusher, y canales autenticados (Private/Presence)
  • UI en vivo con JavaScript mínimo usando Livewire/Alpine/Blade, y diseño de fallback (Polling/SSE)
  • Actualizaciones en vivo accesibles (aria-live, role="status", gestión del foco, toasts/banners/badges)
  • Reconexión práctica, manejo offline, limitación de tasa, autorización, auditoría y testing (Feature/Dusk)
  • Ejemplos para chat, centro de notificaciones, tableros de progreso y agregación de dashboards

Lectores previstos (¿quién se beneficia?)

  • Principiantes e intermedios de Laravel: Construye pantallas dinámicas y en tiempo real (chat, notificaciones, dashboards) de forma segura
  • Líderes técnicos/PMs: Compara costes/operaciones de servicios externos (Pusher) vs. autoalojado (Laravel WebSockets)
  • QA/Accesibilidad: Estandariza anuncios para lectores de pantalla, interacciones con teclado y notificaciones independientes del color
  • CS/Soporte: Mejora el diseño de contenido de notificaciones y progresos para reducir consultas

Nivel de accesibilidad: ★★★★★

Cubrimos implementación para aria-live / role="status" / role="alert", control del foco, estado independiente del color, prefers-reduced-motion, alcance por teclado, estados offline explícitos y reintentos, y más.


1. Introducción: Tiempo real significa equilibrar “conveniencia” con “cambio inesperado”

La UI en tiempo real es útil, pero los cambios repentinos pueden desorientar a las personas.

  • Cuando la pantalla cambia por sí sola, ofrece orientación concisa sobre lo que ocurrió.
  • No muevas el foco a la fuerza al área cambiada (riesgo de interrumpir la entrada).
  • Usa aria-live="polite" por defecto para anuncios; usa role="alert" solo para casos críticos/urgentes.
  • Para desconexiones, reconexiones y latencia, explica discretamente y ofrece siguientes pasos (recargar / modo ligero).

Laravel admite un flujo integral desde eventos del servidor a broadcasting y suscripción del cliente. Este artículo compila diseño, código y operaciones para experiencias en tiempo real seguras y consideradas.


2. Vista general de la arquitectura: evento → canal → entrega → suscripción

  1. Dispara un evento de aplicación (p. ej., OrderCreated).
  2. Retransmite (broadcast) a un canal (público/privado/presence) mediante un evento broadcastable.
  3. Los clientes se suscriben vía WebSockets (Pusher/Laravel WebSockets) o SSE/Polling.
  4. El cliente actualiza la UI (contadores, inserción en lista, toast).

Guía de decisión:

  • Bidireccionalidad, muchas conexiones, latencia ultra-baja → WebSockets.
  • Unidireccional, centrado en difusión, simple → SSE.
  • Entornos restringidos o configuración mínima → Polling.
  • En redes móviles/corporativas con proxies estrictos, combinar SSE/Polling como fallback es más seguro.

3. Fundamentos de Broadcasting: eventos y canales

3.1 Evento broadcastable

// app/Events/OrderCreated.php
namespace App\Events;

use App\Models\Order;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\PrivateChannel; // si se requiere autenticación
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class OrderCreated implements ShouldBroadcast
{
    public function __construct(public Order $order) {}

    public function broadcastOn(): Channel
    {
        // Ejemplo: canal de notificación específico por usuario (autenticado)
        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 Autorización de canal

// routes/channels.php
Broadcast::channel('users.{id}', function ($user, $id) {
    return (int) $user->id === (int) $id; // Solo el propietario puede suscribirse
});
  • Los canales Private/Presence se autorizan en el servidor.
  • Entrega eventos de broadcast mediante colas para manejar picos de carga.

4. Cliente: suscribirse con Laravel Echo

4.1 Cableado básico

npm i laravel-echo pusher-js
// resources/js/bootstrap.js (ejemplo)
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'], // ajusta si usas fallbacks
  authEndpoint: '/broadcasting/auth', // Private/Presence
});

// Ejemplo de suscripción
window.echo.private(`users.${userId}`)
  .listen('.order.created', (e) => {
     // Actualiza la UI
  });

4.2 Laravel WebSockets (autoalojado)

  • Servidor compatible con Pusher que gestionas tú — ideal para control de costes y residencia de datos.
  • Escala pequeña a media puede correr en 1 nodo; escala grande usa sharding horizontal + Redis, etc.

5. SSE (Server-Sent Events): streaming unidireccional sencillo

// 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);
  // Actualiza la UI
};
es.onerror = () => { /* UI de desconexión, etc. */ };
  • Funciona bien sobre HTTP/1.1 y suele ser más amigable con proxies.
  • Si no necesitas bidireccionalidad o Presence, SSE puede ser suficiente.

6. Actualizaciones en vivo accesibles: principios de diseño

  • No muevas encabezados ni el foco automáticamente. Evita interrumpir acciones del usuario.
  • Usa aria-live="polite" para actualizaciones no urgentes; usa role="alert" solo para emergencias.
  • Muestra texto junto con cambios de color (p. ej., conteo en badge).
  • Para toasts, ajusta la prioridad del lector de pantalla (normalmente role="status").
  • Mantén la animación corta y sutil; respeta prefers-reduced-motion.

6.1 Toast mínimo (amigable con lectores de pantalla)

<div id="toast" role="status" aria-live="polite" class="sr-only"></div>

<script>
function announce(msg){ 
  const t = document.getElementById('toast');
  t.textContent = ''; // reanunciar el mismo texto
  setTimeout(()=> t.textContent = msg, 50);
}
</script>

6.2 Insignia de nuevos elementos

<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">Muestra el número de notificaciones nuevas.</p>

<script>
let count = 0;
function onOrderCreated(){ 
  count++;
  document.getElementById('badge').textContent = count;
  announce(`Tienes ${count} pedidos nuevos.`);
}
</script>
  • Combina el color (badge rojo) con números y anuncios.
  • Si al hacer clic se navega a una lista, diseña el siguiente objetivo de foco.

7. UIs típicas: chat y centro de notificaciones

7.1 Chat (canal 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));

// Envío de mensajes: POST habitual → dispara evento en el servidor

Accesibilidad

  • Anuncia mensajes nuevos con texto breve en una región role="status", p. ej., “Mensaje de Yamada”.
  • Usa role="log" para la lista de mensajes para que los lectores manejen mejor el comportamiento de solo anexado.
  • Prefiere texto a sonidos para entradas/salidas; evita sobresaturar.

7.2 Centro de notificaciones

  • Baja prioridad: badge numérico + lista; el badge disminuye al leer.
  • Alta prioridad: toast con role="alert" + recorrido por teclado hacia el detalle.
  • Gestiona caducidad y desduplicación (upsert por clave).

8. Reconexiones, manejo offline, fallbacks

8.1 Banner de estado

<div id="conn" role="status" aria-live="polite" class="text-sm text-gray-600">
  Conectando...
</div>

<script>
function setConn(text, cls=''){ const el = document.getElementById('conn'); el.textContent=text; el.className=cls; }

window.addEventListener('offline', ()=> setConn('Estás sin conexión. Los cambios se guardarán temporalmente.','text-red-700'));
window.addEventListener('online', ()=> setConn('De vuelta en línea. Sincronizando tus cambios.','text-green-700'));
</script>

8.2 Manejo de reconexión en Echo

window.echo.connector.pusher.connection.bind('state_change', (states) => {
  if (states.current === 'connecting') setConn('Conectando...');
  if (states.current === 'unavailable' || states.current === 'disconnected') setConn('La conexión es inestable. Reconectando automáticamente.','text-red-700');
  if (states.current === 'connected') setConn('Conectado','text-green-700');
});

8.3 Estrategia de fallback

  • 1º: WebSockets → 2º: SSE → 3º: Polling (15–60 s).
  • Mantén actualizaciones en vivo solo en pantallas críticas; en el resto ofrece un botón “Actualizar”.
  • Prioriza un diseño donde las funciones sigan funcionando sin actualizaciones en vivo.

9. Seguridad, autorización, limitación de tasa, auditoría

  • Aplica autorización estricta en el servidor para canales Private/Presence.
  • Mantén payloads mínimos (excluye datos sensibles).
  • Aplica Rate Limiting en creación de mensajes/notificaciones para prevenir abuso.
  • Registro de auditoría: quién/cuándo/qué canal/qué tipo de evento.
  • Usa logs estructurados para que IDs de conexión y de usuario sean rastreados.

10. Operaciones del servidor: Horizon/colas, WebSockets, escalado

  • Encola el broadcasting para suavizar picos.
  • Si usas Laravel WebSockets, vigila el dashboard para conexiones/tasa de mensajes.
  • Con muchas conexiones, usa health checks y autoscaling; sirve assets estáticos desde CDN/borde.
  • En proxies inversos (Nginx), ajusta timeouts/cabeceras (para SSE: X-Accel-Buffering: no, etc.).

11. Trabajo/progreso en vivo (dashboard)

11.1 Evento de progreso

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 de progreso accesible

<div aria-labelledby="exp-title">
  <h2 id="exp-title">Progreso de la exportación</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">Aparecerá un enlace de descarga al completar.</p>
</div>

<script>
function updateProgress(p){
  const bar = document.getElementById('bar');
  bar.setAttribute('aria-valuenow', p);
  bar.textContent = `${p}%`;
  if (p===100) announce('Exportación completa.');
}
</script>
  • Muestra números para el progreso; no dependas solo del color.
  • Al completar, notifica con un breve anuncio role="status".

12. Testing: Feature, Dusk, fumadas de a11y

12.1 Feature (disparo de evento)

use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\Facades\Event;

test('payload de OrderCreated broadcast', function () {
    Event::fake();
    $order = Order::factory()->create();
    event(new \App\Events\OrderCreated($order));
    Event::assertDispatched(\App\Events\OrderCreated::class);
});

12.2 Dusk (reflejo en UI y anuncios)

public function test_toast_announces_on_message()
{
    $this->browse(function (Browser $b) {
        $b->visit('/dashboard')
          ->script("announce('Tienes un nuevo pedido')"); // inyecta un mock
        $b->assertSeeIn('#toast','Tienes un nuevo pedido');
    });
}

12.3 Fumadas de a11y (Pa11y/axe)

  • Región de toast presente con role="status".
  • role="alert" no duplicado/sobreutilizado en pantallas no críticas.
  • No hay UI que dependa solo de un color en el badge para el significado.

13. Ejemplo de implementación (contador mínimo de notificaciones)

13.1 Rutas / evento

// 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">Muestra el número de notificaciones nuevas.</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. Errores comunes y cómo evitarlos

  • Usar en exceso alert para actualizaciones no críticas → Prefiere status / polite por defecto.
  • Forzar movimientos de foco en actualizaciones en vivo → No lo hagas; prioriza acciones del usuario.
  • Fallos/silencios de desconexión → Anuncia el estado de conexión brevemente y ofrece rutas de reintento.
  • Autorización débil en Private → Sé estricto en routes/channels.php.
  • Payloads de evento demasiado grandes → Mantenlos mínimos; vuelve a obtener por ID si es necesario.
  • Sin fallback → Proporciona SSE/Polling, además de un claro botón de actualización manual.
  • Significado solo por badge → Incluye números/texto.
  • Demasiada animación → Respeta prefers-reduced-motion; mantenla breve.

15. Checklist (para distribuir al equipo)

Arquitectura

  • [ ] Los eventos implementan ShouldBroadcast; payload mínimo
  • [ ] Autorización en canales Private/Presence
  • [ ] Entrega vía colas; monitoriza con Horizon

Cliente

  • [ ] Cableado de Echo y mensajes de reconexión
  • [ ] Fallbacks SSE/Polling
  • [ ] Estado de conexión visible (role="status")

Accesibilidad

  • [ ] Notificaciones usan role="status" aria-live="polite"; casos urgentes solo usan alert
  • [ ] Badges con números/texto; sin claves solo por color
  • [ ] Evita mover el foco a la fuerza e interrumpir entradas
  • [ ] Respeta prefers-reduced-motion

Seguridad/Ops

  • [ ] Autorización estricta en routes/channels
  • [ ] Rate limiting y logs de auditoría
  • [ ] Copys para estados abajo/arriba/reintento
  • [ ] Monitorización (conexiones/latencia/tasas de error)

Testing

  • [ ] Feature: disparo de eventos/autorización
  • [ ] Dusk: toast/badge/anuncios
  • [ ] a11y: uso adecuado de status/alert

16. Resumen

  • Construye una columna vertebral en tiempo real robusta con Laravel Broadcasting y Echo.
  • Protege con autorización Private/Presence y payloads mínimos.
  • Diseña la UI para notificaciones amigables con lectores de pantalla y estados independientes del color.
  • Proporciona orientación en lenguaje llano para desconexión/reconexión y ofrece fallbacks.
  • Mantén pruebas y monitorización para sostener una experiencia que funcione silenciosa, confiable y amablemente.

Referencias

por greeden

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)