En este tutorial, crearemos una aplicación de chat en tiempo real con Laravel, Nuxt 3, Sanctum y Laravel Reverb para gestionar la mensajería segura y en directo entre usuarios. Configuraremos la autenticación de usuarios, nos conectaremos a una API segura y nos aseguraremos de que el chat se actualice al instante para ofrecer una experiencia fluida y con capacidad de respuesta.
Utilizaremos el módulo para gestionar la autenticación SPA, que gestiona de forma eficiente tanto la autenticación de API como la de aplicaciones de página única (SPA). Para obtener más información sobre el uso de este módulo, consulte el artículo sobre la autenticación SPA de Nuxt 3 .
En este proyecto, configuraremos Laravel Reverb para la transmisión de eventos en tiempo real, implementaremos la autenticación con Sanctum y crearemos una interfaz de Nuxt 3 que muestre y administre mensajes de chat de manera dinámica. ¡Comencemos!
En primer lugar, asegúrese de que Laravel Sanctum esté instalado y configurado. Sanctum permite la autenticación basada en tokens para aplicaciones de una sola página (SPA). Luego, instale y configure Laravel Reverb para obtener capacidades en tiempo real.
Agregue las siguientes variables de entorno de Reverb a su archivo .env
:
REVERB_APP_ID=my-app-id REVERB_APP_KEY=my-app-key REVERB_APP_SECRET=my-app-secret REVERB_HOST="localhost" REVERB_PORT=8080 REVERB_SCHEME=http
Para almacenar mensajes de chat, cree una migración para una tabla chat_messages
. Ejecute:
php artisan make:migration create_chat_messages_table
Actualice el archivo de migración de la siguiente manera:
Schema::create('chat_messages', function (Blueprint $table) { $table->id(); $table->foreignId('receiver_id'); $table->foreignId('sender_id'); $table->text('text'); $table->timestamps(); });
Ejecute la migración para crear la tabla:
php artisan migrate
Para transmitir mensajes en tiempo real, cree una clase de evento MessageSent
:
php artisan make:event MessageSent
Actualizar la clase de evento:
<?php namespace App\Events; use App\Models\ChatMessage; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; class MessageSent implements ShouldBroadcastNow { use Dispatchable, InteractsWithSockets, SerializesModels; public function __construct(public ChatMessage $message) { // } public function broadcastOn(): array { return [new PrivateChannel("chat.{$this->message->receiver_id}")]; } }
En channels.php
, defina el canal de transmisión:
Broadcast::channel('chat.{id}', function ($user, $id) { return (int) $user->id === (int) $id; });
Agregue estas rutas para administrar el envío y la recuperación de mensajes de chat:
Route::get('/messages/{user}', function (User $user, Request $request) { return ChatMessage::query() ->where(function ($query) use ($user, $request) { $query->where('sender_id', $request->user()->id) ->where('receiver_id', $user->id); }) ->orWhere(function ($query) use ($user, $request) { $query->where('sender_id', $user->id) ->where('receiver_id', $request->user()->id); }) ->with(['sender', 'receiver']) ->orderBy('id', 'asc') ->get(); })->middleware('auth:sanctum'); Route::post('/messages/{user}', function (User $user, Request $request) { $request->validate(['message' => 'required|string']); $message = ChatMessage::create([ 'sender_id' => $request->user()->id, 'receiver_id' => $user->id, 'text' => $request->message ]); broadcast(new MessageSent($message)); return $message; });
Para permitir que Nuxt 3 se conecte con Reverb, agregue estas variables a su archivo .env
:
NUXT_PUBLIC_REVERB_APP_ID=my-app-id NUXT_PUBLIC_REVERB_APP_KEY=my-app-key NUXT_PUBLIC_REVERB_APP_SECRET=my-app-secret NUXT_PUBLIC_REVERB_HOST="localhost" NUXT_PUBLIC_REVERB_PORT=8080 NUXT_PUBLIC_REVERB_SCHEME=http
Luego, cárguelos en nuxt.config.ts
:
export default defineNuxtConfig({ runtimeConfig: { public: { REVERB_APP_ID: process.env.NUXT_PUBLIC_REVERB_APP_ID, REVERB_APP_KEY: process.env.NUXT_PUBLIC_REVERB_APP_KEY, REVERB_APP_SECRET: process.env.NUXT_PUBLIC_REVERB_APP_SECRET, REVERB_HOST: process.env.NUXT_PUBLIC_REVERB_HOST, REVERB_PORT: process.env.NUXT_PUBLIC_REVERB_PORT, REVERB_SCHEME: process.env.NUXT_PUBLIC_REVERB_SCHEME, }, }, });
Para administrar actualizaciones en tiempo real, cree un complemento laravel-echo.client.ts
:
import Echo from "laravel-echo"; import Pusher, { type ChannelAuthorizationCallback } from "pusher-js"; declare global { interface Window { Echo: Echo; Pusher: typeof Pusher; } } export default defineNuxtPlugin(() => { window.Pusher = Pusher; const config = useRuntimeConfig(); const echo = new Echo({ broadcaster: "reverb", key: config.public.REVERB_APP_KEY, wsHost: config.public.REVERB_HOST, wsPort: config.public.REVERB_PORT ?? 80, wssPort: config.public.REVERB_PORT ?? 443, forceTLS: (config.public.REVERB_SCHEME ?? "https") === "https", enabledTransports: ["ws", "wss"], authorizer: (channel, options) => ({ authorize: (socketId, callback) => { useSanctumFetch("api/broadcasting/auth", { method: "post", body: { socket_id: socketId, channel_name: channel.name }, }) .then(response => callback(null, response)) .catch(error => callback(error, null)); }, }), }); return { provide: { echo } }; });
Para habilitar el chat en tiempo real en nuestra aplicación Nuxt 3, creé una nueva página, chats > [id].vue
, donde los usuarios pueden seleccionar y chatear con otros usuarios. Esta página se conecta a un backend de Laravel para administrar los mensajes de chat y los datos de los usuarios, utilizando Laravel Echo y WebSockets para actualizaciones en tiempo real e indicadores de escritura.
A continuación se muestra un desglose de cómo estructuramos esta funcionalidad de chat:
Primero, recuperamos el userID
de la URL con el componente useRoute
. Este userID
identifica al usuario con el que estamos chateando y carga los mensajes de usuario y chat necesarios.
const route = useRoute(); const userID = route.params.id; const { user: currentUser } = useSanctum<User>(); const { data: user } = await useAsyncData( `user-${userID}`, () => useSanctumFetch<User>(`/api/users/${userID}`) ); const { data: messages } = useAsyncData( `messages-${userID}`, () => useSanctumFetch<ChatMessage[]>(`/api/messages/${userID}`), { default: (): ChatMessage[] => [] } );
Este fragmento utiliza useSanctumFetch
, un componente personalizado, para cargar mensajes de forma asincrónica cuando se monta la página.
Representamos cada mensaje de forma dinámica y les damos estilo en función de si provienen del usuario actual o del participante del chat.
<div ref="messagesContainer" class="p-4 overflow-y-auto max-h-fit"> <div v-for="message in messages" :key="message.id" class="flex items-center mb-2"> <div v-if="message.sender_id === currentUser.id" class="p-2 ml-auto text-white bg-blue-500 rounded-lg"> {{ message.text }} </div> <div v-else class="p-2 mr-auto bg-gray-200 rounded-lg"> {{ message.text }} </div> </div> </div>
La ventana de chat se desplaza automáticamente al último mensaje usando nextTick()
.
watch( messages, () => { nextTick(() => messageContainerScrollToBottom()); }, { deep: true } ); function messageContainerScrollToBottom() { if (!messagesContainer.value) return; messagesContainer.value.scrollTo({ top: messagesContainer.value.scrollHeight, behavior: 'smooth' }); }
Los usuarios pueden enviar mensajes con un campo de entrada. Una vez enviados, se envía una solicitud POST al backend de Laravel.
const newMessage = ref(""); const sendMessage = async () => { if (!newMessage.value.trim()) return; const messageResponse = await useSanctumFetch<ChatMessage>(`/api/messages/${userID}`, { method: "post", body: { message: newMessage.value } }); messages.value.push(messageResponse); newMessage.value = ""; };
Los mensajes se actualizan inmediatamente después de ser enviados.
Laravel Echo escucha eventos clave como MessageSent
y typing
.
onMounted(() => { if (currentUser.value) { $echo.private(`chat.${currentUser.value.id}`) .listen('MessageSent', (response: { message: ChatMessage }) => { messages.value.push(response.message); }) .listenForWhisper("typing", (response: { userID: number }) => { isUserTyping.value = response.userID === user.value?.id; if (isUserTypingTimer.value) clearTimeout(isUserTypingTimer.value); isUserTypingTimer.value = setTimeout(() => { isUserTyping.value = false; }, 1000); }); } });
Esto maneja las actualizaciones de mensajes en tiempo real y los indicadores de escritura, que desaparecen después de un segundo de inactividad.
Para mostrar un indicador de escritura, activamos un evento de "escritura" con whisper
.
const sendTypingEvent = () => { if (!user.value || !currentUser.value) return; $echo.private(`chat.${user.value.id}`).whisper("typing", { userID: currentUser.value.id }); };
Este evento se envía cada vez que el usuario escribe y el destinatario ve el indicador de escritura.
La interfaz de chat incluye un encabezado con el nombre del usuario seleccionado y secciones para mensajes y el campo de entrada.
<template> <div> <header v-if="user" class="bg-white shadow"> <div class="px-4 py-6 mx-auto max-w-7xl sm:px-6 lg:px-8"> <h1 class="text-2xl font-bold ">{{ user.name }}</h1> </div> </header> <div class="flex flex-col items-center py-5"> <div class="container w-full h-full p-8 space-y-3 bg-white rounded-xl"> <div v-if="currentUser"> <div class="flex flex-col justify-end h-80"> <div ref="messagesContainer" class="p-4 overflow-y-auto max-h-fit"> <div v-for="message in messages" :key="message.id" class="flex items-center mb-2"> <div v-if="message.sender_id === currentUser.id" class="p-2 ml-auto text-white bg-blue-500 rounded-lg"> {{ message.text }} </div> <div v-else class="p-2 mr-auto bg-gray-200 rounded-lg"> {{ message.text }} </div> </div> </div> </div> <div class="flex-shrink-0"> <span v-if="user && isUserTyping" class="text-gray-500"> {{ user.name }} is typing... </span> <div class="flex items-center justify-between w-full p-4 border-t border-gray-200"> <input type="text" v-model="newMessage" @keydown="sendTypingEvent" @keyup.enter="sendMessage" class="w-full p-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Type a message..." /> <button @click.prevent="sendMessage" class="inline-flex items-center justify-center w-12 h-12 ml-4 text-white bg-blue-500 rounded-lg hover:bg-blue-600"> <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" /> </svg> </button> </div> </div> </div> </div> </div> </div> </template>
Esto completa la configuración de la función de chat en tiempo real usando Laravel Sanctum y Echo con Nuxt 3.
Ahora hemos creado una aplicación de chat segura y en tiempo real utilizando Laravel, Sanctum, Reverb y Nuxt 3. Esta configuración se puede escalar fácilmente para incluir funciones adicionales como reacciones a mensajes o múltiples salas de chat.
Para ver el código completo, visita el repositorio de GitHub .