从零构建 Laravel + MySQL 实时聊天室:万字全栈指南 前言 为什么还要自己写一个聊天室? 在 2026 年的今天,实时通信技术早已不是什么新鲜事。市面上有成熟的即时通讯 SDK,也有 Laravel 官方力推的 Reverb。但如果你真的想深入理解 WebSocket 的原理、掌握 Laravel 事件广播的精髓,亲手从零构建一个聊天室仍然是最好的学习路径。
这篇文章不会教你用现成的开源包(比如 Musonza Chat)三分钟搭完收工。我们要做的,是一行一行写代码,从数据库表设计到 WebSocket 服务启动,把每一个环节都拆解清楚。
技术栈是经典的 Laravel + MySQL,实时通信部分我们使用 Laravel Reverb——这是 Laravel 官方自带的 WebSocket 服务器,无需依赖第三方服务如 Pusher,非常适合自建项目。
第一部分:环境准备与项目初始化
1.1 版本要求
开始之前,确认你的环境满足以下条件:
· PHP >= 8.1 · Composer · MySQL >= 5.7 · Node.js & NPM(前端资源编译用) · Git
1.2 创建 Laravel 项目
composer create-project laravel/laravel chat-roomcd chat-room1.3 配置 .env 文件
APP_NAME=ChatRoomAPP_ENV=localAPP_KEY=your-generated-keyAPP_DEBUG=trueAPP_URL=http://localhost:8000
DB_CONNECTION=mysqlDB_HOST=127.0.0.1DB_PORT=3306DB_DATABASE=chat_roomDB_USERNAME=rootDB_PASSWORD=yourpassword生成应用密钥:
php artisan key:generate1.4 安装 Breeze(认证脚手架)
聊天室需要用户身份,我们使用 Laravel Breeze 快速搭建认证系统:
composer require laravel/breeze --devphp artisan breeze:install bladenpm install && npm run devBreeze 会生成登录、注册页面,以及对应的路由和控制器。我们后面会在此基础上扩展聊天功能。
第二部分:数据库设计
2.1 核心表结构
一个聊天室的核心数据模型至少包含三张表:用户表(已有)、聊天室表、消息表。
rooms 表(聊天室)
php artisan make:model Room -m在生成的迁移文件中添加字段:
Schema::create('rooms', function (Blueprint $table) { $table->id(); $table->string('name', 80)->unique(); // 房间名称 $table->text('description')->nullable(); $table->boolean('is_private')->default(false); $table->string('password')->nullable(); // 私密房间密码 $table->foreignId('owner_id')->constrained('users'); // 创建者 $table->timestamps();});messages 表(消息记录)
php artisan make:model Message -mSchema::create('messages', function (Blueprint $table) { $table->id(); $table->text('content'); $table->foreignId('user_id')->constrained()->onDelete('cascade'); $table->foreignId('room_id')->constrained()->onDelete('cascade'); $table->timestamps();});room_user 表(多对多关联)
php artisan make:migration create_room_user_tableSchema::create('room_user', function (Blueprint $table) { $table->id(); $table->foreignId('room_id')->constrained()->onDelete('cascade'); $table->foreignId('user_id')->constrained()->onDelete('cascade'); $table->timestamps();});2.2 运行迁移
php artisan migrate2.3 模型关联
Room.php:
public function users(){ return $this->belongsToMany(User::class)->withTimestamps();}
public function messages(){ return $this->hasMany(Message::class);}
public function owner(){ return $this->belongsTo(User::class, 'owner_id');}User.php:
public function rooms(){ return $this->belongsToMany(Room::class)->withTimestamps();}
public function messages(){ return $this->hasMany(Message::class);}Message.php:
protected $fillable = ['content', 'user_id', 'room_id'];
public function user(){ return $this->belongsTo(User::class);}
public function room(){ return $this->belongsTo(Room::class);}第三部分:安装与配置 Laravel Reverb
Reverb 是 Laravel 11 引入的官方 WebSocket 服务器,基于 Pusher 协议,但完全自托管。
3.1 安装 Reverb
composer require laravel/reverb3.2 安装 Laravel Echo(前端)
npm install --save-dev laravel-echo pusher-js3.3 配置 .env
在 .env 中添加 Reverb 相关配置:
REVERB_APP_ID=your-app-idREVERB_APP_KEY=your-app-keyREVERB_APP_SECRET=your-app-secretREVERB_HOST=127.0.0.1REVERB_PORT=8080REVERB_SCHEME=http
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"VITE_REVERB_HOST="${REVERB_HOST}"VITE_REVERB_PORT="${REVERB_PORT}"VITE_REVERB_SCHEME="${REVERB_SCHEME}"生成应用 ID、Key 和 Secret:
php artisan reverb:install该命令会自动生成并填充这些值。
3.4 配置广播驱动
修改 config/broadcasting.php 中的 default 为 reverb:
'default' => env('BROADCAST_DRIVER', 'reverb'),确保 connections.reverb 配置正确:
'reverb' => [ 'driver' => 'reverb', 'key' => env('REVERB_APP_KEY'), 'secret' => env('REVERB_APP_SECRET'), 'app_id' => env('REVERB_APP_ID'), 'options' => [ 'host' => env('REVERB_HOST', 'localhost'), 'port' => env('REVERB_PORT', 8080), 'scheme' => env('REVERB_SCHEME', 'http'), ],],第四部分:后端核心逻辑
4.1 聊天室 CRUD
创建 RoomController:
php artisan make:controller RoomController --resourceroutes/web.php:
use App\Http\Controllers\RoomController;
Route::middleware(['auth'])->group(function () { Route::resource('rooms', RoomController::class)->except(['show']); Route::get('/chat/{room}', [RoomController::class, 'show'])->name('rooms.show'); Route::post('/rooms/{room}/join', [RoomController::class, 'join'])->name('rooms.join');});RoomController 核心方法:
public function index(){ $rooms = Room::withCount('users')->get(); return view('rooms.index', compact('rooms'));}
public function store(Request $request){ $validated = $request->validate([ 'name' => 'required|string|max:80|unique:rooms', 'description' => 'nullable|string', 'is_private' => 'boolean', 'password' => 'nullable|string|min:4', ]);
$room = Room::create([ ...$validated, 'owner_id' => auth()->id(), ]);
// 创建者自动加入房间 $room->users()->attach(auth()->id());
return redirect()->route('rooms.show', $room);}
public function join(Request $request, Room $room){ if ($room->is_private && $request->password !== $room->password) { return back()->withErrors(['password' => '房间密码错误']); }
$room->users()->syncWithoutDetaching([auth()->id()]); return redirect()->route('rooms.show', $room);}4.2 消息事件
这是实时通信的核心。当用户发送消息时,我们触发一个事件,Reverb 会将该事件广播给所有监听该频道的客户端。
首先创建消息事件:
php artisan make:event MessageSentapp/Events/MessageSent.php:
use Illuminate\Broadcasting\Channel;use Illuminate\Broadcasting\InteractsWithSockets;use Illuminate\Contracts\Broadcasting\ShouldBroadcast;use App\Models\Message;
class MessageSent implements ShouldBroadcast{ use InteractsWithSockets;
public $message;
public function __construct(Message $message) { $this->message = $message->load('user'); }
public function broadcastOn() { // 每个房间作为一个独立频道 return new Channel('room.' . $this->message->room_id); }
public function broadcastWith() { return [ 'id' => $this->message->id, 'content' => $this->message->content, 'user' => [ 'id' => $this->message->user->id, 'name' => $this->message->user->name, ], 'created_at' => $this->message->created_at->toISOString(), ]; }}ShouldBroadcast 接口告诉 Laravel 这个事件需要被广播。broadcastOn 方法定义事件发送到哪个频道。
4.3 消息控制器
创建消息控制器处理发送请求:
php artisan make:controller MessageControlleruse App\Models\Message;use App\Models\Room;use App\Events\MessageSent;
class MessageController extends Controller{ public function store(Request $request, Room $room) { $validated = $request->validate([ 'content' => 'required|string|max:1000', ]);
// 检查用户是否在该房间 if (!$room->users()->where('user_id', auth()->id())->exists()) { abort(403, '您未加入此聊天室'); }
$message = Message::create([ 'content' => $validated['content'], 'user_id' => auth()->id(), 'room_id' => $room->id, ]);
// 触发广播事件 broadcast(new MessageSent($message));
return response()->json($message->load('user'), 201); }
public function index(Room $room) { // 加载最近50条消息,用于历史记录 $messages = $room->messages() ->with('user') ->latest() ->limit(50) ->get() ->reverse();
return response()->json($messages); }}routes/api.php(或 web.php):
use App\Http\Controllers\MessageController;
Route::middleware(['auth'])->group(function () { Route::get('/rooms/{room}/messages', [MessageController::class, 'index']); Route::post('/rooms/{room}/messages', [MessageController::class, 'store']);});第五部分:前端实现
5.1 配置 Laravel Echo
在 resources/js/bootstrap.js 中初始化 Echo:
import Echo from 'laravel-echo';import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({ broadcaster: 'reverb', key: import.meta.env.VITE_REVERB_APP_KEY, wsHost: import.meta.env.VITE_REVERB_HOST, wsPort: import.meta.env.VITE_REVERB_PORT, wssPort: import.meta.env.VITE_REVERB_PORT, forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https', enabledTransports: ['ws', 'wss'],});5.2 聊天页面视图
创建一个 Blade 视图 resources/views/rooms/show.blade.php:
@extends('layouts.app')
@section('content')<div class="container mx-auto px-4"> <div class="flex flex-col h-[80vh]"> <!-- 房间头部 --> <div class="border-b pb-4 mb-4"> <h1 class="text-2xl font-bold">{{ $room->name }}</h1> <p class="text-gray-600">{{ $room->description }}</p> </div>
<!-- 消息列表 --> <div id="messages" class="flex-1 overflow-y-auto space-y-4 mb-4 p-4 bg-gray-50 rounded-lg"> <!-- 消息将动态渲染到这里 --> </div>
<!-- 输入框 --> <div class="flex gap-2"> <input type="text" id="message-input" placeholder="输入消息..." class="flex-1 rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"> <button id="send-btn" class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"> 发送 </button> </div> </div></div>
@push('scripts')<script> // 后续写入</script>@endpush@endsection5.3 前端 JavaScript 逻辑
在页面中编写完整的聊天逻辑:
import axios from 'axios';
const roomId = {{ $room->id }};const messagesContainer = document.getElementById('messages');const messageInput = document.getElementById('message-input');const sendBtn = document.getElementById('send-btn');
// 1. 加载历史消息async function loadHistory() { try { const response = await axios.get(`/rooms/${roomId}/messages`); response.data.forEach(msg => appendMessage(msg, false)); } catch (error) { console.error('加载历史消息失败:', error); }}
// 2. 追加消息到界面function appendMessage(message, isNew = true) { const div = document.createElement('div'); div.className = `flex flex-col ${isNew ? 'animate-fade-in' : ''}`; div.innerHTML = ` <div class="flex items-center gap-2"> <span class="font-semibold text-blue-600">${message.user.name}</span> <span class="text-xs text-gray-400">${new Date(message.created_at).toLocaleTimeString()}</span> </div> <div class="ml-4 p-2 bg-white rounded-lg shadow-sm border border-gray-100 inline-block"> ${escapeHtml(message.content)} </div> `; messagesContainer.appendChild(div); messagesContainer.scrollTop = messagesContainer.scrollHeight;}
// 简单的 XSS 防护function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML;}
// 3. 发送消息async function sendMessage() { const content = messageInput.value.trim(); if (!content) return;
try { await axios.post(`/rooms/${roomId}/messages`, { content }); messageInput.value = ''; messageInput.focus(); } catch (error) { alert(error.response?.data?.message || '发送失败,请重试'); }}
// 4. 监听实时消息window.Echo.channel(`room.${roomId}`) .listen('MessageSent', (event) => { appendMessage(event, true); });
// 5. 事件绑定sendBtn.addEventListener('click', sendMessage);messageInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }});
// 6. 启动loadHistory();5.4 编译前端资源
npm run build开发环境可以用 npm run dev 启动 Vite 热更新。
第六部分:启动服务
6.1 启动 Reverb WebSocket 服务器
打开终端,启动 Reverb:
php artisan reverb:start默认会在 127.0.0.1:8080 启动 WebSocket 服务。如果想后台运行,可以加上 —host=0.0.0.0 允许外部访问。
6.2 启动 Laravel 开发服务器
另开一个终端:
php artisan serve6.3 测试流程
- 注册两个以上账号(可以用不同浏览器或隐私模式)
- 创建一个聊天室
- 让其他用户加入该房间
- 互相发送消息,观察实时推送效果
第七部分:进阶功能与优化
7.1 在线用户列表
使用 Reverb 的 Presence Channel 可以跟踪房间内在线用户。它比私有频道多了一个 here 事件,能自动告知房间内的其他用户状态。
修改事件定义:
use Illuminate\Broadcasting\PresenceChannel;
public function broadcastOn(){ return new PresenceChannel('room.' . $this->message->room_id);}前端监听:
window.Echo.join(`room.${roomId}`) .here((users) => { // 显示当前在线用户 }) .joining((user) => { // 用户加入 }) .leaving((user) => { // 用户离开 }) .listen('MessageSent', (event) => { appendMessage(event); });7.2 已读回执与输入中状态
这些功能需要额外的事件和数据库字段:
· 消息表增加 read_at 字段,标记已读时间 · 输入状态通过 Typing 事件广播,前端用 debounce 控制发送频率
7.3 消息限流与内容过滤
在高并发场景下,需要对消息发送进行限流。Laravel 内置的 RateLimiter 可以实现:
use Illuminate\Support\Facades\RateLimiter;
public function store(Request $request, Room $room){ $key = 'send-message:' . auth()->id();
if (RateLimiter::tooManyAttempts($key, 40)) { return response()->json([ 'message' => '发送过于频繁,请稍后再试' ], 429); }
RateLimiter::hit($key, 60); // ... 继续发送逻辑}一个生产级项目通常会配合 XSS 过滤和敏感词屏蔽,保证内容安全。
7.4 水平扩展
当单台 Reverb 服务器无法承载时,可以部署多台 Reverb 实例,使用 Redis 作为广播后端,实现跨服务器的消息同步:
REVERB_REDIS_HOST=127.0.0.1REVERB_REDIS_PORT=6379常见问题排查
WebSocket 连接失败
- 确认 Reverb 服务已启动:php artisan reverb
- 检查 .env 中 REVERB_PORT 与前端 VITE_REVERB_PORT 一致
- 检查防火墙是否开放对应端口
- 在浏览器开发者工具 Network → WS 标签中查看连接状态
消息收不到
- 确认 broadcast() 函数被调用
- 确认频道名称前后端一致(注意大小写)
- 检查 Laravel 日志 storage/logs/laravel.log
- 确认队列驱动正常(如果使用队列)
数据库连接问题
- 确认 MySQL 服务运行中
- 检查 .env 中的数据库凭据
- 确认数据库 chat_room 已创建
- 运行 php artisan migrate
—seed 重置
总结
至此,我们已经完成了一个基于 Laravel + MySQL + Reverb 的实时聊天室。从数据库设计到后端事件广播,再到前端 Echo 监听,每一步都经过了实战检验。
这个项目可以作为一个良好的起点,你可以在此基础上扩展:
· 私聊功能(一对一频道) · 文件/图片发送 · 消息搜索 · 聊天机器人集成
实时通信的世界远不止于此,希望这篇文章能帮你打下坚实的基础,开启属于自己的实时应用之旅。
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 48 天
部分内容可能已过时
评论