2416 字
12 分钟
从零构建 Laravel + MySQL 实时聊天室:万字全栈指南

从零构建 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 项目

Terminal window
composer create-project laravel/laravel chat-room
cd chat-room

1.3 配置 .env 文件

APP_NAME=ChatRoom
APP_ENV=local
APP_KEY=your-generated-key
APP_DEBUG=true
APP_URL=http://localhost:8000
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=chat_room
DB_USERNAME=root
DB_PASSWORD=yourpassword

生成应用密钥:

Terminal window
php artisan key:generate

1.4 安装 Breeze(认证脚手架)

聊天室需要用户身份,我们使用 Laravel Breeze 快速搭建认证系统:

Terminal window
composer require laravel/breeze --dev
php artisan breeze:install blade
npm install && npm run dev

Breeze 会生成登录、注册页面,以及对应的路由和控制器。我们后面会在此基础上扩展聊天功能。


第二部分:数据库设计

2.1 核心表结构

一个聊天室的核心数据模型至少包含三张表:用户表(已有)、聊天室表、消息表。

rooms 表(聊天室)

Terminal window
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 表(消息记录)

Terminal window
php artisan make:model Message -m
Schema::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 表(多对多关联)

Terminal window
php artisan make:migration create_room_user_table
Schema::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 运行迁移

Terminal window
php artisan migrate

2.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

Terminal window
composer require laravel/reverb

3.2 安装 Laravel Echo(前端)

Terminal window
npm install --save-dev laravel-echo pusher-js

3.3 配置 .env

在 .env 中添加 Reverb 相关配置:

REVERB_APP_ID=your-app-id
REVERB_APP_KEY=your-app-key
REVERB_APP_SECRET=your-app-secret
REVERB_HOST=127.0.0.1
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}"

生成应用 ID、Key 和 Secret:

Terminal window
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:

Terminal window
php artisan make:controller RoomController --resource

routes/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 会将该事件广播给所有监听该频道的客户端。

首先创建消息事件:

Terminal window
php artisan make:event MessageSent

app/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 消息控制器

创建消息控制器处理发送请求:

Terminal window
php artisan make:controller MessageController
use 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
@endsection

5.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 编译前端资源

Terminal window
npm run build

开发环境可以用 npm run dev 启动 Vite 热更新。


第六部分:启动服务

6.1 启动 Reverb WebSocket 服务器

打开终端,启动 Reverb:

Terminal window
php artisan reverb:start

默认会在 127.0.0.1:8080 启动 WebSocket 服务。如果想后台运行,可以加上 —host=0.0.0.0 允许外部访问。

6.2 启动 Laravel 开发服务器

另开一个终端:

Terminal window
php artisan serve

6.3 测试流程

  1. 注册两个以上账号(可以用不同浏览器或隐私模式)
  2. 创建一个聊天室
  3. 让其他用户加入该房间
  4. 互相发送消息,观察实时推送效果

第七部分:进阶功能与优化

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.1
REVERB_REDIS_PORT=6379

常见问题排查

WebSocket 连接失败

  1. 确认 Reverb 服务已启动:php artisan reverb
  2. 检查 .env 中 REVERB_PORT 与前端 VITE_REVERB_PORT 一致
  3. 检查防火墙是否开放对应端口
  4. 在浏览器开发者工具 Network → WS 标签中查看连接状态

消息收不到

  1. 确认 broadcast() 函数被调用
  2. 确认频道名称前后端一致(注意大小写)
  3. 检查 Laravel 日志 storage/logs/laravel.log
  4. 确认队列驱动正常(如果使用队列)

数据库连接问题

  1. 确认 MySQL 服务运行中
  2. 检查 .env 中的数据库凭据
  3. 确认数据库 chat_room 已创建
  4. 运行 php artisan migrate —seed 重置

总结

至此,我们已经完成了一个基于 Laravel + MySQL + Reverb 的实时聊天室。从数据库设计到后端事件广播,再到前端 Echo 监听,每一步都经过了实战检验。

这个项目可以作为一个良好的起点,你可以在此基础上扩展:

· 私聊功能(一对一频道) · 文件/图片发送 · 消息搜索 · 聊天机器人集成

实时通信的世界远不止于此,希望这篇文章能帮你打下坚实的基础,开启属于自己的实时应用之旅。

从零构建 Laravel + MySQL 实时聊天室:万字全栈指南
https://iyu6.com/posts/laraveltochat/
作者
yuyuovo
发布于
2026-08-14
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取

评论

正在加载评论...