2024 年 3 月,发布了 Laravel 11。随之而来的是 Laravel 生态系统中的一个新工具:Laravel Reverb。
Reverb 是一个独立的开源包,它是 Laravel 应用程序的第一方 WebSocket 服务器。它有助于促进客户端和服务器之间的实时通信。
在此新包之前,Laravel 有事件广播,但基本上它没有内置的方法来设置自托管 WebSocket 服务器。幸运的是,Reverb 现在为我们提供了该选项。
Laravel Reverb 具有几个关键特性:它是用 PHP 编写的,速度快,可扩展。它特别被开发为水平可扩展。
Reverb 基本上允许你在单个服务器上运行应用程序——但是如果应用程序开始超出该服务器的承载能力,你可以添加多个额外的服务器。然后,这些服务器可以相互通信,在它们之间分发消息。
在本文中,您将学习如何使用 Laravel Reverb 构建实时聊天应用程序。这将使您能够轻松地在后端和前端之间实现 WebSocket 通信。
对于前端技术,你可以使用任何你想要的东西——但在这种情况下,我们将使用 React.js 和 Vite.js 构建工具。
到本文结束时,你将在本地机器上拥有一个全栈实时应用程序,它将如下工作:
应用程序演示,展示两个已登录用户之间的消息传递
目录
前提条件
你将需要以下工具来开发我们将在本文中构建的应用程序:
- PHP:版本 8.2 或更高版本(运行
php -v
检查版本)
- 作曲家(运行
composer
检查它是否存在)
- Node.js:版本 20 或更高版本(运行
node -v
检查版本)
- MySQL:版本 5.7 或更高版本(运行
mysql --version
检查是否存在,或按照文档进行安装)
一般步骤
本文的主要步骤如下:
如何安装 Laravel
首先,使用 composer 命令安装 Laravel 11:
composer create-project laravel/laravel:^11.0 laravel-reverb-react-chat && cd laravel-reverb-react-chat/
此时,你可以通过运行 serve
命令来查看应用程序
php artisan serve
如何创建模型和迁移
你可以使用以下单个命令生成消息的模型和迁移:
php artisan make:model -m Message
然后,你需要使用以下代码设置消息的模型:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Message extends Model
{
use HasFactory;
public $table = 'messages';
protected $fillable = ['id', 'user_id', 'text'];
public function user(): BelongsTo {
return $this->belongsTo(User::class, 'user_id');
}
public function getTimeAttribute(): string {
return date(
"d M Y, H:i:s",
strtotime($this->attributes['created_at'])
);
}
}
如你所见,有一个 ` getTimeAttribute()
` 访问器,它将把消息创建时间戳格式化为人类可读的日期和时间格式。它将在聊天框中的每条消息的顶部显示。
接下来,使用此代码设置 messages
数据库表的迁移:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void {
Schema::create('messages', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->text('text')->nullable();
$table->timestamps();
});
}
public function down(): void {
Schema::dropIfExists('messages');
}
};
这个迁移在数据库中创建了一个 messages
表。该表包含用于自动递增主键( id
)、外键( user_id
)引用 users
表的 id
列、用于存储消息内容的 text
列以及 timestamps
,以自动跟踪每个记录的创建和修改时间。
迁移还包括回滚方法( down()
),如果需要,可以删除 messages
表。
在本文中,我们将使用 MySQL 数据库,但如果您喜欢,也可以使用默认的 SQLite。只需确保在 .env
文件中正确设置数据库凭据:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=database_name
DB_USERNAME=username
DB_PASSWORD=password
设置完环境变量后,优化缓存:
php artisan optimize
运行迁移以重新创建数据库表以及添加 messages
表:
php artisan migrate:fresh
如何添加身份验证
现在,你可以为你的应用添加身份验证支架。你可以使用 Laravel 的 UI 包来导入一些资产文件。首先,你需要安装相应的包:
composer require laravel/ui
然后将 React 相关的资源导入到应用中:
php artisan ui react --auth
它可能会提示是否覆盖 app/Http/Controllers/Controller.php
,你可以允许它这样做
The [Controller.php] file already exists. Do you want to replace it? (yes/no) [no]
这将完成所有已编译和安装的身份验证支架,包括路由、控制器、视图、Vite 配置,以及一个简单的 React 特定示例。\
此时,应用程序已准备就绪,您只需再进行一步操作即可。
注意:请确保已安装 Node.js(带 npm)版本 20 或更高版本。你可以通过运行 node -v
命令来检查。否则,只需按照官方页面进行安装即可。
npm install && npm run build
上述命令将安装 NPM 包并构建前端资产。现在,您可以启动 Laravel 应用程序并查看您的完整准备好的应用程序示例:
php artisan optimize && php artisan serve
注册页面的截图
也请注意,当您对前端文件进行更改时,您可以单独运行 dev
命令,而不必每次都使用 build
:
npm run dev
查看 package.json
文件中的详细信息,在 scripts
字段中。
如何设置路线
在这个实时聊天应用程序中,你需要有几个路由:
home
用于主页(已添加)
message
用于添加新消息
messages
获取所有现有消息
你将在 web.php
文件中找到这些类型的路线:
<?php
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\HomeController;
Route::get('/', function () { return view('welcome'); });
Auth::routes();
Route::get('/home', [HomeController::class, 'index'])
->name('home');
Route::get('/messages', [HomeController::class, 'messages'])
->name('messages');
Route::post('/message', [HomeController::class, 'message'])
->name('message');
设置好这些路由后,让我们利用 Laravel 事件和队列作业的优势。
如何设置 Laravel 事件
你需要创建一个 GotMessage
事件来监听一个特定的事件
php artisan make:event GotMessage
Laravel 的事件提供了一种简单的观察者模式实现,允许你订阅和监听应用程序中发生的各种事件。事件类通常存储在 app/Events
目录中。(文档)
为所有已认证用户在 broadcastOn
方法中设置一个私有 WebSocket 通道,以实时接收消息。在这种情况下,我们将其称为 "channel_for_everyone"
,但您也可以根据用户动态设置,例如 "App.Models.User.{$this->message['user_id']}"
。
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class GotMessage implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public array $message) {
//
}
public function broadcastOn(): array {
// $this->message is available here
return [
new PrivateChannel("channel_for_everyone"),
];
}
}
如你所见,有一个公共的 $massage
属性作为构造函数参数,因此你可以在前端获取消息信息。
我们已经在 channels 文件中使用了频道名称,并且我们也将在前端使用它来进行实时消息更新。
别忘了在事件类中实现 ShouldBroadcast
接口。
如何设置 Laravel 队列作业
现在是时候创建发送消息的 SendMessage
作业了:
php artisan make:job SendMessage
Laravel 允许你轻松创建可以在后台处理的排队作业。通过将耗时的任务转移到队列中,你的应用程序可以快速响应用户的 Web 请求,并为你的客户提供更好的用户体验。(文档)
<?php
namespace App\Jobs;
use App\Events\GotMessage;
use App\Models\Message;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SendMessage implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Message $message) {
//
}
public function handle(): void {
GotMessage::dispatch([
'id' => $this->message->id,
'user_id' => $this->message->user_id,
'text' => $this->message->text,
'time' => $this->message->time,
]);
}
}
SendMessage.php
队列作业负责分发带有新发送消息信息的 GotMessage
事件。它在构造时接收一个 Message
对象,表示要发送的消息。
在其 handle()
方法中,它派发了 GotMessage
事件,其中包含消息 ID、用户 ID、文本和时间戳等详细信息。这个任务被设计为异步队列处理,以便在后台高效地处理消息发送任务。
如你所见,有一个公共的 $massage
属性作为构造函数参数,我们将使用它来将消息信息附加到队列作业。
如何编写控制器方法
对于定义的路由,这里有相应的控制器方法:
<?php
namespace App\Http\Controllers;
use App\Jobs\SendMessage;
use App\Models\Message;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class HomeController extends Controller
{
public function __construct() {
$this->middleware('auth');
}
public function index() {
$user = User::where('id', auth()->id())->select([
'id', 'name', 'email',
])->first();
return view('home', [
'user' => $user,
]);
}
public function messages(): JsonResponse {
$messages = Message::with('user')->get()->append('time');
return response()->json($messages);
}
public function message(Request $request): JsonResponse {
$message = Message::create([
'user_id' => auth()->id(),
'text' => $request->get('text'),
]);
SendMessage::dispatch($message);
return response()->json([
'success' => true,
'message' => "Message created and job dispatched.",
]);
}
}
- 在
home
方法中,我们将使用 User
模型从数据库中获取登录用户的数据,并将其发送到 blade 视图。
- 在
messages
方法中,我们将使用 Message
模型从数据库中检索所有消息,将 user
关系数据附加到它,为每个项目添加 time
字段(访问器),并将所有内容发送到视图。
- 在
message
方法中,将使用 Message
模型在数据库表中创建新消息,并调度 SendMessage
队列作业。
如何安装 Laravel Reverb
现在我们来到了最重要的时刻:是时候在你的 Laravel 应用中安装 Reverb 了。
这太简单了。所有必要的打包和配置设置都可以使用这一个命令完成:
php artisan install:broadcasting
它将要求你安装 Laravel Reverb,以及安装和构建广播所需的 Node 依赖项。只需按回车键继续。
命令执行后,请确保已自动将特定于混响的环境变量添加到 .env
文件中,例如:
BROADCAST_CONNECTION=reverb
###
REVERB_APP_ID=795051
REVERB_APP_KEY=s3w3thzezulgp5g0e5bs
REVERB_APP_SECRET=gncsnk3rzpvczdakl6pz
REVERB_HOST="localhost"
REVERB_PORT=8080
REVERB_SCHEME=http
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
你也会在 config
目录下看到两个新的配置文件:
reverb.php
broadcasting.php
如何设置 WebSocket 通道
最后,你需要在 channels.php
文件中添加一个频道。在安装 Reverb 后,它应该已经创建了。
<?php
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('channel_for_everyone', function ($user) {
return true;
});
你将只有一个频道。你可以改变频道的名称并使其具有动态性——由你决定。在频道关闭时,我们将始终返回 true,但你可以稍后修改它,以对频道的订阅施加一些限制。
再优化一次缓存
php artisan optimize
如何自定义 Laravel 视图
现在后端应该已经准备好了,所以你可以切换到前端。
在处理 React 相关内容之前,你需要先设置 Laravel *.blade.php
的视图。在 home
的 blade 视图中,确保有一个 ID 为 main
的根 div,以便在那里渲染所有的 React 组件。
@extends('layouts.app')
@section('content')
<div class="container">
<div id="main" data-user="{{ json_encode($user) }}"></div>
</div>
@endsection
该具有 ID 的 div 获得一个数据属性,用于保存从控制器的 home
方法发送的 $user
信息。
我不会在这里放整个 resources/views/welcome.blade.php
的内容,但你可以对其进行以下小的改动:
- 将
url('/dashboard')
替换为 url('/home')
- 将
Dashboard
替换为 Home
- 删除
main
和 footer
部分。
让我们从前端开始工作
在 Reverb 中,事件广播是由服务器端广播驱动程序完成的,该驱动程序广播您的 Laravel 事件,以便前端可以在浏览器客户端中接收它们。
在前端,Laravel Echo 在幕后完成了这项工作。Echo 是一个 JavaScript 库,它使得订阅频道和监听服务器端广播驱动程序广播的事件变得轻而易举。
你可以在 rources/js/echo.js
文件中找到与 Echo 一起设置的 WebSocket 配置,但对于这个项目,你不需要在那里做任何事情。
让我们创建一些 React 组件,以便我们拥有一个经过重构的、更具可读性的项目。
在新的 components
文件夹中创建一个 Main.jsx
组件
import React from 'react';
import ReactDOM from 'react-dom/client';
import '../../css/app.css';
import ChatBox from "./ChatBox.jsx";
if (document.getElementById('main')) {
const rootUrl = "http://127.0.0.1:8000";
ReactDOM.createRoot(document.getElementById('main')).render(
<React.StrictMode>
<ChatBox rootUrl={rootUrl} />
</React.StrictMode>
);
}
如果存在 ID 为 'main'
的元素,它将继续呈现 React 应用程序。
如你所见,这里有一个 ChatBox
组件。我们很快会学习更多关于它的内容。
删除 resources/js/components/Example.jsx
文件,并导入 Main.jsx
组件到 app.js
中
import './bootstrap';
import './components/Main.jsx';
创建 Message.jsx
和 MessageInput.jsx
文件,以便在 ChatBox
组件中使用它们。
Message
组件将获取 userId
和 message
参数(字段),以在聊天框中显示每条消息。
import React from "react";
const Message = ({ userId, message }) => {
return (
<div className={`row ${
userId === message.user_id ? "justify-content-end" : ""
}`}>
<div className="col-md-6">
<small className="text-muted">
<strong>{message.user.name} | </strong>
</small>
<small className="text-muted float-right">
{message.time}
</small>
<div className={`alert alert-${
userId === message.user_id ? "primary" : "secondary"
}`} role="alert">
{message.text}
</div>
</div>
</div>
);
};
export default Message;
Message.jsx
组件在聊天界面中呈现各个消息。它接收 userId
和 message
属性。根据消息发送者是否与当前用户匹配,它将消息对齐到屏幕的相应一侧。
每条消息都包含发件人的姓名、时间戳和消息内容本身,根据消息是由当前用户还是其他用户发送,样式有所不同。
MessageInput
组件将负责创建新消息
import React, { useState } from "react";
const MessageInput = ({ rootUrl }) => {
const [message, setMessage] = useState("");
const messageRequest = async (text) => {
try {
await axios.post(`${rootUrl}/message`, {
text,
});
} catch (err) {
console.log(err.message);
}
};
const sendMessage = (e) => {
e.preventDefault();
if (message.trim() === "") {
alert("Please enter a message!");
return;
}
messageRequest(message);
setMessage("");
};
return (
<div className="input-group">
<input onChange={(e) => setMessage(e.target.value)}
autoComplete="off"
type="text"
className="form-control"
placeholder="Message..."
value={message}
/>
<div className="input-group-append">
<button onClick={(e) => sendMessage(e)}
className="btn btn-primary"
type="button">Send</button>
</div>
</div>
);
};
export default MessageInput;
MessageInput
组件为用户提供了一个输入框,用于在聊天界面中输入消息并发送。点击按钮,它会触发一个函数,通过 Axios POST 请求将消息发送到服务器,请求的目标地址是从父组件 ChatBox
中获取的 rootUrl
。它还处理了验证,以确保用户不能发送空消息。如果需要,您可以稍后进行自定义。
现在创建一个 ChatBox.jsx
组件,让前端准备好:
import React, { useEffect, useRef, useState } from "react";
import Message from "./Message.jsx";
import MessageInput from "./MessageInput.jsx";
const ChatBox = ({ rootUrl }) => {
const userData = document.getElementById('main')
.getAttribute('data-user');
const user = JSON.parse(userData);
// `App.Models.User.${user.id}`;
const webSocketChannel = `channel_for_everyone`;
const [messages, setMessages] = useState([]);
const scroll = useRef();
const scrollToBottom = () => {
scroll.current.scrollIntoView({ behavior: "smooth" });
};
const connectWebSocket = () => {
window.Echo.private(webSocketChannel)
.listen('GotMessage', async (e) => {
// e.message
await getMessages();
});
}
const getMessages = async () => {
try {
const m = await axios.get(`${rootUrl}/messages`);
setMessages(m.data);
setTimeout(scrollToBottom, 0);
} catch (err) {
console.log(err.message);
}
};
useEffect(() => {
getMessages();
connectWebSocket();
return () => {
window.Echo.leave(webSocketChannel);
}
}, []);
return (
<div className="row justify-content-center">
<div className="col-md-8">
<div className="card">
<div className="card-header">Chat Box</div>
<div className="card-body"
style={{height: "500px", overflowY: "auto"}}>
{
messages?.map((message) => (
<Message key={message.id}
userId={user.id}
message={message}
/>
))
}
<span ref={scroll}></span>
</div>
<div className="card-footer">
<MessageInput rootUrl={rootUrl} />
</div>
</div>
</div>
</div>
);
};
export default ChatBox;
ChatBox
组件管理应用程序内的聊天界面。它使用 WebSocket 和 HTTP 请求从服务器获取和显示消息。
该组件呈现一个消息列表、一个消息输入字段,并且当新消息到达时自动滚动到底部。
它定义了一个用于实时消息更新的 WebSocket 通道。您需要使用与 routes/hannels.php
和 app/Events/GotMessage.php
队列作业中相同的名称设置该通道。
此外, leave()
函数在 useEffect
清理函数中被调用,以在组件卸载时取消订阅 WebSocket 通道。这通过在组件不再需要时停止侦听 WebSocket 通道上的更新,防止内存泄漏和不必要的网络连接。
运行应用程序
现在,一切准备就绪,是时候查看应用程序了。请按照以下说明进行操作:
终端的屏幕截图,其中包含所有必要的命令
- 构建前端资源(这不是一个“永久”运行的命令):\
npm run build
- 开始监听 Laravel 事件\
php artisan queue:listen
- 启动 WebSocket 服务器\
php artisan reverb:start
- 启动服务器(你可以使用你的应用程序的替代方案,如本地运行的服务器):\
php artisan serve
运行完所有必要的命令后,您可以通过访问默认 URL: http://127.0.0.1:8000
. 来查看应用。
用于测试,您可以注册两个不同的用户,让这些用户登录,从每个用户发送消息,并查看聊天框。
有用的混响资源
既然我们已经到了这篇文章的结尾,值得列出一些关于 Reverb 的有用资源:
- Laravel 广播(官方文档)
- 泰勒·奥特威尔 - Laravel 更新(在 Laracon EU 2024 上的演讲)
- 乔·迪克森(Reverb 的创建者)在 X 上
- 拉卡斯特剧集(带 Reverb 的实际示例)
结论
现在你知道如何使用新版本的 Laravel 中的 Laravel Reverb 构建实时应用程序了。有了这个,你可以在你的全栈应用程序中实现 WebSocket 通信,而无需使用任何额外的第三方服务(如 Pusher 和 Socket.io)。
如果你想在不使用任何额外的 Laravel 工具(如 Inertia)的情况下将 React.js 集成到你的 Laravel 应用中,你可以阅读我之前在 freeCodeCamp 上的文章,在那里你可以构建一个单页面、全栈的 Tasklist 应用。
本文的完整代码在这里我的 GitHub⭐上,我在那里积极宣传我关于各种现代技术的许多工作。
https://github.com/boolfalse/laravel-reverb-react-chat