在当今的数字环境中,实时通信应用程序已成为最大限度提升用户体验的关键。即时消息、即时客户支持和协作工具使用户能够快速高效地进行交流,从而简化企业和个人的日常运营。在本文中,你将了解如何使用功能强大的 Laravel Reverb 库和灵活高效的 Vue 3 框架构建实时聊天应用程序。
什么是 Laravel Reverb?
Laravel Reverb 是适用于 Laravel 应用程序的第一方 WebSocket 服务器,可以实现客户端与服务器之间的实时通信。这个功能强大的工具可以让开发人员在自己的 Laravel 项目中直接轻松实现即时消息、实时通知和实时更新等功能。
安装 Laravel 11
你可以使用 PHP 的依赖关系管理器 Composer 安装 Laravel 11,方法是打开终端或命令提示符,执行以下命令:
composer global require laravel/installer
laravel new chat-app
在项目设置过程中,你可以选择 Laravel Breeze 作为身份验证的入门套件,为处理用户身份验证提供一个简单的解决方案。Laravel Breeze 不仅提供了简洁明了的身份验证脚手架,还能为你自动安装和配置 Tailwind CSS。
运行数据库迁移,生成必要的表。
php artisan migrate
执行以下命令创建六个示例用户:
php artisan tinker
App\Models\User :: factory ( 6 )-> create ();
Seed a user with tinker
用户已创建,现在就可以登录了。
集成 Laravel Reverb
默认情况下,新的 Laravel 应用程序不会启用广播功能。您可以使用 install:broadcasting Artisan
命令启用广播功能:
php artisan install:broadcasting
install:broadcasting
命令将创建 config/broadcasting.php
配置文件。此外,该命令还将创建 routes/channels.php 文件,您可以在其中注册应用程序的广播授权路由和回调。
安装 Laravel Reverb
创建模型和迁移
我们需要创建一个模型,以便在数据库中存储聊天信息。
运行以下命令创建模型和迁移:
php artisan make:model Message -m
模型和迁移文件已经创建。让我们更新迁移文件如下。
public function up(): void
{
Schema::create('messages', function (Blueprint $table) {
$table->id();
$table->foreignId('sender_id');
$table->foreignId('receiver_id');
$table->text('text');
$table->timestamps();
});
}
在 Message
类中添加 $fillable 属性和关系。
class Message extends Model
{
use HasFactory;
protected $fillable = [
'sender_id',
'receiver_id',
'text'
];
public function sender()
{
return $this->belongsTo(User::class, 'sender_id');
}
public function receiver()
{
return $this->belongsTo(User::class, 'receiver_id');
}
}
创建路由和控制器
在 routes/web.php 中添加以下路由:
Route::middleware(['auth'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
Route::get('/chat/{user}', [ChatController::class, 'show'])->name('chat');
Route::get('/messages/{user}', [ChatController::class, 'getMessages']);
Route::post('/messages/{user}', [ChatController::class, 'sendMessage']);
});
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\User;
class DashboardController extends Controller
{
public function index()
{
$users = User::whereNot('id', auth()->id())->get();
return view('dashboard', compact('users'));
}
}
index
方法检索除当前经过身份验证的用户之外的所有用户,并允许经过身份验证的用户选择他们想要与谁聊天。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\User;
use App\Models\Message;
use App\Events\MessageSent;
class ChatController extends Controller
{
public function index(User $user)
{
return Message::query()
->where(function ($query) use ($user) {
$query->where('sender_id', auth()->id())
->where('receiver_id', $user->id);
})
->orWhere(function ($query) use ($user) {
$query->where('sender_id', $user->id)
->where('receiver_id', auth()->id());
})
->with(['sender', 'receiver'])
->orderBy('created_at', 'asc')
->get();
}
public function show(User $user)
{
return view('chat', [
'user' => $user
]);
}
public function sendMessage(Request $request, User $user)
{
$message = Message::create([
'sender_id' => auth()->id(),
'receiver_id' => $user->id,
'text' => $request->input('message')
]);
broadcast(new MessageSent($message));
return response()->json($message);
}
}
index
方法获取两个用户之间发送的所有消息。
store
方法只是创建一条新消息。
broadcast(new MessageSent($message));
会向所有连接的客户端实时发送 MessageSent
事件,确保聊天界面即时更新新消息。
创建 MessageSent 事件
在 Laravel 中创建 MessageSent
事件,运行以下 Artisan 命令:
php artisan make:event MessageSent
这将创建一个新文件app/Events/MessageSent.php
。使用以下内容更新该文件:
<?php
namespace App\Events;
use App\Models\Message;
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;
use InteractsWithSockets;
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Message $message)
{
//
}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel("chat.{$this->message->receiver_id}"),
];
}
}
在 channels.php
中设置广播频道
<?php
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('chat.{id}', function ($user, $id) {
return (int) $user->id === (int) $id;
});
Broadcast::channel('presence.chat', function ($user) {
return ['id' => $user->id, 'name' => $user->name];
});
第一个通道,chat.{id}
,确保只有通过身份验证的用户才能加入{id}指定的聊天室。
第二个通道,presence.chat
,管理聊天室中所有用户的在线信息,提供在线用户的实时更新。
安装 Vue
下面的命令可确保已安装最新版本的 Vue.js,并可在项目中使用:
npm install vue@latest
npm install --save-dev @vitejs/plugin-vue
命令用于将 Vue.js 插件添加到 Vite 项目中。Vite 是一款快速、现代的构建工具,旨在通过即时启动服务器和快速热模块替换(HMR)等功能改善开发体验。
安装所有这些软件包后,您的 vite.config.js 文件应如下所示。
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [
laravel({
input: [
'resources/css/app.css',
'resources/js/app.js',
],
refresh: true,
}),
vue({
template: {
transformAssetUrls: {
base: null,
includeAbsolute: false,
},
},
}),
],
resolve: {
alias: {
vue: 'vue/dist/vue.esm-bundler.js',
},
},
});
接下来,我们在项目的主 Blade 文件resources/views/layouts/app.blade.php
中添加id="app"
:
< body class = “font-sans antialiased” >
< div class = “min-h-screen bg-gray-100” id = “app” >
resources/views/dashboard.blade.php
文件应更新如下。在这里,经过身份验证的用户查看可以与之聊天的用户列表,并被引导至聊天页面。
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Dashboard') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
{{ __("You're logged in!") }}
</div>
</div>
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg mt-6">
<div class="p-6 text-gray-900">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
@foreach ($users as $user)
<a href="{{ route('chat', $user->id) }}" class="bg-gray-100 p-4 rounded-lg shadow-md block hover:bg-gray-200">
<h3 class="text-lg font-semibold">{{ $user->name }}</h3>
<p>{{ $user->email }}</p>
</a>
@endforeach
</div>
</div>
</div>
</div>
</div>
</x-app-layout>
仪表板
首先,我们将在 resources/js/components
目录下创建一个名为 ChatComponent.vue
的文件。该文件将包含处理聊天界面功能的 Vue.js 组件。
现在,我们可以在 app 元素内创建 Vue 应用程序。下面是 Vue 3 版本的语法,位于 resources/js/app.js
目录中:
import './bootstrap';
import { createApp } from 'vue'
import ChatComponent from './components/ChatComponent.vue'
const app = createApp({});
app.component('chat-component', ChatComponent);
app.mount('#app');
要创建聊天界面,我们将在 resources/views
目录下创建 chat.blade.php
文件。
<!-- resources/views/chat.blade.php -->
<x-app-layout>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<chat-component :user="{{ $user }}" :current-user="{{ auth()->user() }}"></chat-component>
</div>
</div>
</div>
</div>
</x-app-layout>
Blade 模板中的 <chat-component :user="{{ $user }}" :current-user="{{ auth()->user() }}"></chat-component>
一行嵌入了一个 Vue.js 组件,并向其传递了两个数据:正在聊天的用户($user
)和当前已验证的用户(auth()->user()
)。这样,Vue.js 组件就能动态显示和管理聊天界面。
聊天组件
<template>
<div class="flex flex-col h-[500px]">
<div class="flex items-center">
<h1 class="text-lg font-semibold mr-2">{{ user.name }}</h1>
<span :class="isUserOnline ? 'bg-green-500' : 'bg-gray-400'" class="inline-block h-2 w-2 rounded-full"></span>
</div>
<!-- Messages -->
<div ref="messageContainer" class="overflow-y-auto p-4 mt-3 flex-grow border-t border-gray-200">
<div class="space-y-4">
<div
v-for="message in messages"
:key="message.id"
:class="{ 'text-right': message.sender_id === currentUser.id }"
class="mb-4"
>
<div
:class="message.sender_id === currentUser.id ? 'bg-indigo-500 text-white' : 'bg-gray-200 text-gray-800'"
class="inline-block px-5 py-2 rounded-lg"
>
<p>{{ message.text }}</p>
<span class="text-[10px]">{{ formatTime(message.created_at) }}</span>
</div>
</div>
</div>
</div>
<!-- Message Input -->
<div class="border-t pt-4">
<form @submit.prevent="sendMessage">
<div class="flex items-center">
<input
v-model="newMessage"
@keydown="sendTypingEvent"
type="text"
class="flex-1 border p-3 rounded-lg"
placeholder="Type your message here..."
/>
<button type="submit" class="ml-2 bg-indigo-500 text-white p-3 rounded-lg shadow hover:bg-indigo-600 transition duration-300 flex items-center justify-center">
<i class="fas fa-paper-plane"></i>
<span class="ml-2">Send</span>
</button>
</div>
</form>
</div>
</div>
<small v-if="isUserTyping" class="text-gray-600 mt-5">
{{ user.name }} is typing...
</small>
</template>
<script setup>
import { ref, onMounted, watch, nextTick } from 'vue';
import axios from 'axios';
const props = defineProps({
user: {
type: Object,
required: true
},
currentUser: {
type: Object,
required: true
}
});
const messages = ref([]);
const newMessage = ref('');
const messageContainer = ref(null)
const isUserTyping = ref(false);
const isUserTypingTimer = ref(null);
const isUserOnline = ref(false);
watch(
messages,
() => {
nextTick(() => {
messageContainer.value.scrollTo({
top: messageContainer.value.scrollHeight,
behavior: "smooth",
});
});
},
{ deep: true }
);
const fetchMessages = async () => {
try {
const response = await axios.get(`/messages/${props.user.id}`);
messages.value = response.data;
} catch (error) {
console.error("Failed to fetch messages:", error);
}
};
const sendMessage = async () => {
if (newMessage.value.trim() !== '') {
try {
const response = await axios.post(`/messages/${props.user.id}`, {
message: newMessage.value,
});
messages.value.push(response.data);
newMessage.value = '';
} catch (error) {
console.error("Failed to send message:", error);
}
}
};
const sendTypingEvent = () => {
Echo.private(`chat.${props.user.id}`).whisper("typing", {
userID: props.currentUser.id,
});
};
const formatTime = (datetime) => {
const options = { hour: '2-digit', minute: '2-digit' };
return new Date(datetime).toLocaleTimeString([], options);
};
onMounted(() => {
fetchMessages();
Echo.join(`presence.chat`)
.here(users => {
isUserOnline.value = users.some(user => user.id === props.user.id);
})
.joining(user => {
if (user.id === props.user.id) isUserOnline.value = true;
})
.leaving(user => {
if (user.id === props.user.id) isUserOnline.value = false;
});
Echo.private(`chat.${props.currentUser.id}`)
.listen("MessageSent", (response) => {
messages.value.push(response.message);
})
.listenForWhisper("typing", (response) => {
isUserTyping.value = response.userID === props.user.id;
if (isUserTypingTimer.value) {
clearTimeout(isUserTypingTimer.value);
}
isUserTypingTimer.value = setTimeout(() => {
isUserTyping.value = false;
}, 1000);
});
});
</script>
watch
用于监控 messages
变量的变化。当 messages
发生变化时,nextTick
会确保 DOM 更新完成,然后将消息容器平滑滚动到底部。
fetchMessages
函数从服务器获取指定用户的消息,并更新messages
变量。如果发生错误,它会记录错误并提醒用户。
sendMessage
函数将 newMessage
的内容发送到服务器,并将响应添加到 messages
变量中。如果发生错误,它会记录错误并提醒用户。
sendTypingEvent
函数发送一个 “typing “事件,通知其他用户当前用户正在打字。该函数使用 Laravel Echo 管理实时事件。
formatTime
函数将日期时间字符串格式化为更易读的时间格式,仅显示小时和分钟。
onMounted
在组件安装时运行:
fetchMessages
被调用来从服务器加载消息。
Echo.join
订阅在线状态频道来追踪哪些用户在线。
Echo.private
订阅私有频道以监听新消息和打字事件。当发送新消息时,它会添加到变量中messages
。当收到打字事件时,它会isUserTyping
使用计时器更新状态以重置它。
设置好 Laravel Reverb 并创建必要的 Vue 组件后,需要运行两个重要命令来启动开发环境并测试你的实时聊天应用程序:
php artisan reverb:start
npm run dev
聊天组件
使用 Laravel Reverb 和 Vue 3 构建实时聊天应用程序,展示了将强大的后端框架与现代前端库相结合的强大功能和灵活性。Laravel Reverb 提供了无缝的 WebSocket 集成,实现了即时消息和实时通知等实时通信功能。Vue 3 采用反应式数据绑定和基于组件的架构,增强了用户体验。