Laravel Reverb là máy chủ WebSocket chính thức của Laravel, giúp tích hợp tính năng thời gian thực một cách dễ dàng. Nó cho phép gửi và nhận dữ liệu tức thì mà không cần tải lại trang, mang đến trải nghiệm mượt mà và nhanh chóng.
Trong bài viết này, chúng ta sẽ xây dựng một ứng dụng chat đơn giản, nơi các tin nhắn được gửi ngay lập tức, giúp người dùng tương tác hiệu quả hơn.
Thiết Lập Dự Án Laravel
- Yêu Cầu Trước Khi Bắt Đầu
Để xây dựng ứng dụng trong bài viết này, bạn cần chuẩn bị các công cụ sau:
- PHP: Phiên bản 8.2 trở lên (Kiểm tra bằng lệnh: php -v)
- Composer: Đảm bảo Composer đã được cài đặt (Kiểm tra bằng lệnh: composer)
- Node.js: Phiên bản 20 trở lên (Kiểm tra bằng lệnh: node -v)
- Đảm bảo rằng bạn đã có một ứng dụng Laravel (Laravel 10.x trở lên).
- Nếu bắt đầu từ đầu, chạy lệnh:
composer create-project laravel/laravel laravel-reverb-vue
Hiện tại mình đang sử dụng (Laravel 11.x)
Cài Đặt & Cấu Hình Laravel Reverb
Cài đặt Laravel Reverb bằng lệnh:
php artisan install:broadcasting
Sau khi cài đặt, cập nhật file .env
...
REVERB_APP_ID=my-app-id
REVERB_APP_KEY=my-app-key
REVERB_APP_SECRET=my-app-secret
REVERB_SCHEME=http
...
Cấu Hình Echo
Quá trình cài đặt cũng tự động tạo file echo.js trong thư mục resources/js.
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 ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
})
Tham khảo Laravel Documentation để biết thêm chi tiết về các bước cấu hình cụ thể.
Chạy Máy Chủ Reverb
Bạn có thể khởi chạy máy chủ Reverb bằng lệnh:
php artisan reverb:start
Mặc định, máy chủ Reverb sẽ chạy tại địa chỉ 0.0.0.0:8080, nghĩa là nó có thể truy cập từ tất cả các giao diện mạng.
Nếu bạn muốn chỉ định host hoặc port cụ thể, sử dụng các tùy chọn --host và --port
php artisan reverb:start --host=127.0.0.1 --port=9000
Ngoài ra, bạn cũng có thể định nghĩa các biến môi trường REVERB_HOST và REVERB_PORT trong file .env của ứng dụng.
...
REVERB_HOST="localhost"
REVERB_PORT=8080
...
Thiết Lập Cơ Sở Dữ Liệu
Mở file .env và điều chỉnh các cài đặt để thiết lập cơ sở dữ liệu. Ví dụ, sử dụng SQLite
DB_CONNECTION=sqlite
Tạo file database:
touch database.sqlite
Tạo model & migration:
php artisan make:model ChatMessage --migration
Cập nhật migration:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('chat_messages', function (Blueprint $table) {
$table->id();
$table->foreignId('receiver_id');
$table->foreignId('sender_id');
$table->text('text');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('chat_messages');
}
};
Chạy migrate:
php artisan migrate
Cập nhật model ChatMessage.php:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ChatMessage 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');
}
}
Tạo Sự Kiện
Tạo sự kiện MessageSent.php:
php artisan make:event MessageSent
<?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;
/**
* Create a new event instance.
*/
public function __construct(public ChatMessage $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}")
];
}
}
Định Nghĩa Kênh Private
<?php
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('chat.{id}', function ($user, $id) {
return (int) $user->id === (int) $id;
});
Đoạn mã này định nghĩa một kênh private có tên chat.{id} sử dụng facade Broadcast của Laravel. Các kênh private giúp hạn chế truy cập dựa trên logic xác thực và ủy quyền người dùng.
Định Nghĩa Các Route
// ...
Route::get('/chat/{friend}', function (User $friend) {
return view('chat', [
'friend' => $friend
]);
})->middleware(['auth'])->name('chat');
Route::get('/messages/{friend}', function (User $friend) {
return ChatMessage::query()
->where(function ($query) use ($friend) {
$query->where('sender_id', Auth::id())
->where('receiver_id', $friend->id);
})
->orWhere(function ($query) use ($friend) {
$query->where('sender_id', $friend->id)
->where('receiver_id', Auth::id());
})
->with(['sender', 'receiver'])
->orderBy('id', 'asc')
->get();
})->middleware(['auth']);
Route::post('/messages/{friend}', function (User $friend) {
$message = ChatMessage::create([
'sender_id' => Auth::id(),
'receiver_id' => $friend->id,
'text' => request()->input('message')
]);
broadcast(new MessageSent($message));
return $message;
});
// ...
- GET ('/chat/{friend}'): Chịu trách nhiệm render giao diện chat và nhận tham số động {friend} đại diện cho đối tác chat của người dùng.
- GET ('/messages/{friend}'): Truy xuất các tin nhắn được trao đổi giữa người dùng đã xác thực và đối tác được chỉ định ({friend}). Truy vấn đảm bảo lấy ra các tin nhắn mà người dùng là người gửi hoặc người nhận, bao gồm cả hai hướng của cuộc trò chuyện.
- POST ('/messages/{friend}'): Sau khi tạo tin nhắn, lệnh broadcast(new MessageSent($message)) sử dụng tính năng phát sóng của Laravel để gửi tin nhắn mới này tới tất cả người dùng kết nối thông qua Reverb, từ đó kích hoạt tính năng chat thời gian thực.
Tạo Giao Diện Blade
Để render giao diện chat, bạn cần tạo một file view Blade. Tạo file mới có tên chat.blade.php trong thư mục resources/views và thêm đoạn mã sau:
<x-app-layout>
<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800">
{{ $friend->name }}
</h2>
</x-slot>
<div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div class="overflow-hidden bg-white shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
<chat-component
:friend="{{ $friend }}"
:current-user="{{ auth()->user() }}"
/>
</div>
</div>
</div>
</div>
</x-app-layout>
Điểm mấu chốt ở đây là dòng:
<chat-component :friend="{{ $friend }}" :current-user="{{ auth()->user() }}" />
Dòng này render một component Vue.js có tên chat-component.
Tạo ChatComponent
Ví dụ tạo một component ChatComponent.vue để quản lý giao diện và hành vi động của chat:
<template>
<div>
<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 items-center">
<input
type="text"
v-model="newMessage"
@keydown="sendTypingEvent"
@keyup.enter="sendMessage"
placeholder="Type a message..."
class="flex-1 px-2 py-1 border rounded-lg"
/>
<button
@click="sendMessage"
class="px-4 py-1 ml-2 text-white bg-blue-500 rounded-lg"
>
Send
</button>
</div>
<small v-if="isFriendTyping" class="text-gray-700">
{{ friend.name }} is typing...
</small>
</div>
</template>
<script setup>
import axios from 'axios'
import { nextTick, onMounted, ref, watch } from 'vue'
const props = defineProps({
friend: {
type: Object,
required: true,
},
currentUser: {
type: Object,
required: true,
},
})
const messages = ref([])
const newMessage = ref('')
const messagesContainer = ref(null)
const isFriendTyping = ref(false)
const isFriendTypingTimer = ref(null)
watch(
messages,
() => {
nextTick(() => {
messagesContainer.value.scrollTo({
top: messagesContainer.value.scrollHeight,
behavior: 'smooth',
})
})
},
{ deep: true },
)
const sendMessage = () => {
if (newMessage.value.trim() !== '') {
axios
.post(`/messages/${props.friend.id}`, {
message: newMessage.value,
})
.then((response) => {
messages.value.push(response.data)
newMessage.value = ''
})
}
}
const sendTypingEvent = () => {
Echo.private(`chat.${props.friend.id}`).whisper('typing', {
userID: props.currentUser.id,
})
}
onMounted(() => {
axios.get(`/messages/${props.friend.id}`).then((response) => {
console.log(response.data)
messages.value = response.data
})
Echo.private(`chat.${props.currentUser.id}`)
.listen('MessageSent', (response) => {
messages.value.push(response.message)
})
.listenForWhisper('typing', (response) => {
isFriendTyping.value = response.userID === props.friend.id
if (isFriendTypingTimer.value) {
clearTimeout(isFriendTypingTimer.value)
}
isFriendTypingTimer.value = setTimeout(() => {
isFriendTyping.value = false
}, 1000)
})
})
</script>
Component Vue.js này quản lý giao diện chat với các tính năng
- Hiển thị danh sách tin nhắn có khả năng cuộn, được định dạng khác nhau tùy theo người gửi (người dùng hiện tại hoặc bạn bè).
- Cung cấp ô nhập liệu cho việc soạn tin và nút gửi tin.
- Sử dụng axios để gửi các yêu cầu HTTP nhằm lấy danh sách tin nhắn ban đầu và gửi tin nhắn mới.
- Tính năng thời gian thực được thực hiện qua Laravel Echo
- Lắng nghe các sự kiện MessageSent được phát sóng để cập nhật danh sách tin nhắn khi có tin nhắn mới.
- Sử dụng whisper trên các kênh private để thông báo cho đối tác khi người dùng đang gõ và nhận thông báo gõ từ bạn bè.
Kết luận
Laravel Reverb giúp xây dựng các ứng dụng real-time nhanh chóng và mạnh mẽ. Với bài viết này, bạn có thể tạo một hệ thống chat đơn giản nhưng hiệu quả.
Tài liệu tham khảo:
- Laravel Reverb
- Easy Laravel Reverb Setup For Beginners
- How to Build a Real-Time Chat App with Laravel Reverb
Mã nguồn tham khảo: Laravel Reverb Chat