How to use Laravel Reverb
Author: | ADMIN |
---|
Laravel Reverb là một máy chủ WebSocket chính thức cho các ứng dụng Laravel, cung cấp khả năng giao tiếp theo thời gian thực giữa máy khách và máy chủ một cách liền mạch.
Laravel Reverb có nhiều tính năng hấp dẫn, bao gồm:
- Tốc độ và khả năng mở rộng.
- Hỗ trợ hàng nghìn kết nối đồng thời.
- Tích hợp với các tính năng phát sóng hiện có của Laravel.
- Tương thích với Laravel Echo.
- Tích hợp và triển khai hàng đầu với Laravel Forge.
Trong hướng dẫn này, tôi sẽ hướng dẫn bạn cách sử dụng Laravel Reverb để phát triển một ứng dụng Laravel thời gian thực. Bạn sẽ học về các kênh, sự kiện, phát sóng, và cách sử dụng Laravel Reverb để tạo ra các ứng dụng nhanh chóng và thời gian thực trong Laravel.
Hơn nữa, bạn sẽ học cách thêm thông báo thời gian thực vào ứng dụng Laravel Reverb của mình!
- Cài đặt một ứng dụng Laravel mới
composer create-project laravel/laravel laravel-reverb-chat
Chuyển đến thư mục dự án của bạn:
cd laravel-reverb-chat
- Installing Laravel Reverb
Cài đặt Laravel Reverb bằng cách chạy câu lệnh sau:
php artisan install:broadcasting npm install --save laravel-echo pusher-js
Chú ý: các lựa chọn default
Sau khi bạn đã cài đặt Reverb, bạn có thể chỉnh sửa cấu hình của nó từ tệp`config / reverb.php`.
Để thiết lập kết nối đến Reverb, một bộ thông tin đăng nhập ứng dụng Reverb phải được trao đổi giữa máy khách và máy chủ. Những thông tin đăng nhập này được cấu hình trên máy chủ và được sử dụng để xác minh yêu cầu từ máy khách. Bạn có thể xác định những thông tin đăng nhập này bằng cách sử dụng các biến môi trường sau:BROADCAST_DRIVER=reverb REVERB_APP_ID=my-app-id REVERB_APP_KEY=my-app-key REVERB_APP_SECRET=my-app-secret
- Running Server
Bạn có thể khởi chạy máy chủ Reverb bằng cách sử dụng commandreverb:start
:
php artisan reverb:start
Mặc định, máy chủ Reverb sẽ được khởi động tại
0.0.0.0:8080
, điều này làm cho nó có thể truy cập từ tất cả các giao diện mạng. Nếu bạn muốn thiết lập một máy chủ hoặc cổng cụ thể, bạn có thể sử dụng các tùy chọn --host và --port khi khởi động máy chủ.php artisan reverb:start --host=127.0.0.1 --port=9000
Bạn cũng có thể xác định các biến môi trường REVERB_SERVER_HOST và REVERB_SERVER_PORT trong tệp cấu hình
.env
của ứng dụng của bạn. - Setup Database
Mở tệp .env của bạn và điều chỉnh cài đặt để thiết lập cơ sở dữ liệu của bạn. Dưới đây là một ví dụ sử dụng MySQL để đơn giản hóa:
DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=laravel DB_USERNAME=root DB_PASSWORD=
Đối với bản demo này, chúng ta sẽ tạo năm phòng được xác định trước. Hãy bắt đầu bằng cách tạo một migration cho một bảng rooms.
php artisan make:model Room --migration
Để đơn giản hóa, chỉ cần tạo trường
name
cho model này và chạy migration.Schema::create('rooms', function (Blueprint $table) { $table->id(); $table->string('name'); $table->timestamps(); });
php artisan migrate
Sau đó, seed dữ liệu cho database với 5 room. Tạo một seeder:
php artisan make:seeder RoomsTableSeeder
Run seeder:<?php namespace Database\Seeders; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; class RoomsTableSeeder extends Seeder { /** * Run the database seeds. */ public function run(): void { DB::table('rooms')->insert([ ['name' => 'Room 1'], ['name' => 'Room 2'], ['name' => 'Room 3'], ['name' => 'Room 4'], ['name' => 'Room 5'], ]); } }
php artisan db:seed --class=RoomsTableSeeder
- Tạo Event
Trong thư mụcapp/Events
, tạo một file mới tên làMessageSent.php
. File này chịu trách nhiệm phát sóng các tin nhắn mới tới các phòng chat cụ thể. Dưới đây là mẫu cơ bản:
php artisan make:event MessageSent
class MessageSent implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; public $userName; public $roomId; public $message; public function __construct($userName, $roomId, $message) { $this->userName = $userName; $this->roomId = $roomId; $this->message = $message; } public function broadcastOn() : Channel { return new Channel('chat.' . $this->roomId); } public function broadcastWith() { return [ 'userName' => $this->userName, 'message' => $this->message, ]; } }
- Tạo Pages
Trong dự án này, chúng ta sẽ có hai trang: một trang để hiển thị danh sách các phòng và một trang cho từng phòng chat riêng biệt. Chúng ta sẽ bắt đầu bằng cách tạo các template Blade để hiển thị các phòng. Đặt tên các view này làindex.blade.php
vàchat.blade.php
và lưu chúng trong thư mụcrooms
dướiresources/views
. Tiếp theo, chúng ta sẽ tạo một controller và một route để điều hướng tới các trang này.
php artisan make:view rooms/index php artisan make:view rooms/chat
index.blade.php
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Chat Rooms</title> </head> <body> <div id="app"> <h1>Chat Rooms</h1> <ul> @foreach ($rooms as $room) <li> <a href="{{ route('rooms.show', $room->id) }}">Join {{ $room->name }}</a> </li> @endforeach </ul> </div> </body> </html>
chat.blade.php
Thiết lập một form cơ bản để hiển thị các tin nhắn và một trường nhập liệu đơn giản để gửi tin nhắn. Đảm bảo rằng bạn đã import Echo và Pusher vào file
app.js
.<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Chat Room: {{ $room->name }}</title> @vite(['resources/css/app.css']) @vite(['resources/js/app.js']) </head> <body> <div id="app"> <h2>Chat Room: {{ $room->name }}</h2> <div id="messages" style="border: 1px solid #ccc; margin-bottom: 10px; padding: 10px; height: 300px; overflow-y: scroll;"> <!-- Messages will be displayed here --> </div> <input type="text" id="messageInput" placeholder="Type your message here..." autofocus> <button onclick="sendMessage()">Send</button> </div> <script> document.addEventListener('DOMContentLoaded', function() { const roomId = "{{ $room->id }}"; Echo.channel(`chat.${roomId}`) .listen('MessageSent', (e) => { const messages = document.getElementById('messages'); const messageElement = document.createElement('div'); messageElement.innerHTML = `<strong>${e.userName}:</strong> ${e.message}`; messages.appendChild(messageElement); messages.scrollTop = messages.scrollHeight; // Scroll to the bottom }); }) function sendMessage() { const messageInput = document.getElementById('messageInput'); const message = messageInput.value; messageInput.value = ''; // Clear input const roomId = "{{ $room->id }}" fetch(`/rooms/${roomId}/message`, { method: 'POST', headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}', 'Content-Type': 'application/json' }, body: JSON.stringify({ message: message }) }).catch(error => console.error('Error:', error)); } </script> </body> </html>
app.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'], });
Tiếp theo hãy tạo Controller
php artisan make:controller RoomsController
<?php namespace App\Http\Controllers; use App\Models\Room; class RoomsController extends Controller { public function index() { $rooms = Room::all(); return view('rooms.index', [ 'rooms' => $rooms ]); } public function show(Room $room) { return view('rooms.chat', [ 'roomId' => $room->id, 'room' => $room, 'messages' => [] ]); } }
Để đơn giản, hãy tạo một endpoint
postMessage
và thêm nó vàoweb.php
.php artisan make:controller ChatController
routes/web.php<?php namespace App\Http\Controllers; use App\Events\MessageSent; use Illuminate\Http\Request; use Illuminate\Support\Str; class ChatController extends Controller { public function postMessage(Request $request, $roomId) { $userName = 'User_' . Str::random(4); $messageContent = $request->input('message'); MessageSent::dispatch($userName, $roomId, $messageContent); return response()->json(['status' => 'Message sent successfully.']); } }
Route::get('/rooms', [RoomsController::class, 'index'])->name('rooms.index'); Route::get('/rooms/{room}', [RoomsController::class, 'show'])->name('rooms.show'); Route::post('/rooms/{roomId}/message', [ChatController::class, 'postMessage'])->name('api.rooms.message.post');
- Run Project
Để chạy dự án Laravel, chúng ta cần thực thi các lệnh sau:
- Khởi động Laravel:php artisan serve
- Khởi động frontend:
npm run dev
- Bắt đầu queue:
php artisan queue:listen
- Run Reverb:
php artisan reverb:start
Để biết thông tin chi tiết hơn, bạn có thể kiểm tra tài liệu hướng dẫn chính thức của Laravel Reverb. Chúc các bạn thành công
Ví dụ về các vấn đề truy vấn N+1
Author: | ADMIN |
---|
# Vấn Đề N+1 Query Trong Eloquent
Vấn đề N+1 query là một trong những vấn đề phổ biến mà các nhà phát triển gặp phải khi làm việc với ORM như Eloquent trong Laravel. Vấn đề này xảy ra khi ứng dụng của bạn thực hiện một lượng lớn các truy vấn nhỏ lặp đi lặp lại để lấy dữ liệu liên quan, thay vì chỉ thực hiện một vài truy vấn lớn hơn, dẫn đến giảm hiệu suất ứng dụng. Dưới đây là bốn ví dụ về vấn đề N+1 query và cách giải quyết chúng.
Trước khi vào các ví dụ cụ thể, bạn nên cài đặt Laravel Debugbar để có thể kiểm tra và debug các ứng dụng Laravel một cách dễ dàng. Debugbar hiển thị các thông tin chi tiết về các truy vấn cơ sở dữ liệu, các biến session, các yêu cầu HTTP và nhiều thông tin khác ngay trên giao diện của ứng dụng.
composer require barryvdh/laravel-debugbar --dev
Tiếp theo, bạn chỉ cần kích hoạt chế độ gỡ lỗi với biến APP_DEBUG=true trong file .env
// .env
APP_DEBUG=true
Ví Dụ 1: Lấy Dữ Liệu Liên Quan Trong Vòng Lặp
Mã Gặp Vấn Đề N+1 Query:
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name;
}
Giải Thích:
Đoạn mã trên sẽ thực hiện một truy vấn để lấy tất cả các bài viết. Sau đó, nó sẽ thực hiện một truy vấn riêng lẻ cho mỗi bài viết để lấy thông tin người dùng liên quan, dẫn đến tổng cộng 1 + N truy vấn (N là số lượng bài viết).
Giải Quyết Sử Dụng Eager Loading:
$posts = Post::with('user')->get();
foreach ($posts as $post) {
echo $post->user->name;
}
Giải Thích:
Bằng cách sử dụng phương thức with
, chúng ta chỉ thực hiện hai truy vấn: một truy vấn để lấy tất cả các bài viết và một truy vấn để lấy tất cả các người dùng liên quan.
Ví Dụ 2: Ký hiệu
Giả sử bạn có cùng mối quan hệ hasMany giữa tác giả và sách và bạn cần liệt kê các tác giả cùng số lượng sách của mỗi tác giả.
// Controller
public function index()
{
$authors = Author::with('books')->get();
return view('authors.index', compact('authors'));
}
Và sau đó, trong tệp Blade, bạn thực hiện một vòng lặp foreach cho bảng:
@foreach($authors as $author)
<tr>
<td>{{ $author->name }}</td>
<td>{{ $author->books()->count() }}</td>
</tr>
@endforeach
Mọi thứ trông có vẻ hợp lý, đúng không? Nhưng nhìn vào dữ liệu Debugbar bên dưới.
Chúng ta đang sử dụng eager loading, Author::with('books')
, nhưng tại sao lại có nhiều truy vấn xảy ra như thế?
Bởi vì, trong Blade, $author->books()->count()
không load mối quan hệ từ bộ nhớ.
$author->books()
có nghĩa là PHƯƠNG THỨC của mối quan hệ$author->books
có nghĩa là DỮ LIỆU được eager loaded vào bộ nhớ
Vậy, phương pháp liên kết sẽ truy vấn cơ sở dữ liệu cho mỗi tác giả. Nhưng nếu bạn tải dữ liệu mà không có dấu ngoặc "()", nó sẽ thành công sử dụng dữ liệu được eager loading:
Vì vậy, hãy chú ý đến chính xác những gì bạn đang sử dụng - phương pháp quan hệ hay dữ liệu.
Lưu ý rằng trong ví dụ cụ thể này có một giải pháp thậm chí còn tốt hơn. Nếu bạn chỉ cần dữ liệu tổng hợp được tính toán của mối quan hệ, mà không cần mô hình đầy đủ, thì bạn chỉ nên tải các tổng hợp, như withCount:
// Controller:
$authors = Author::withCount('books')->get();
// Blade:
{{ $author->books_count }}
Kết quả sẽ chỉ có MỘT truy vấn đến cơ sở dữ liệu, thậm chí không phải là hai truy vấn. Và bộ nhớ cũng sẽ không bị "polluted" với dữ liệu quan hệ, do đó cũng tiết kiệm được một số RAM.
Ví Dụ 3: Mối quan hệ "ẩn" trong Accessor
Hãy lấy một ví dụ tương tự: danh sách các tác giả, với cột cho biết tác giả có hoạt động hay không: "Có" hoặc "Không". Hoạt động đó được xác định bởi việc tác giả có ít nhất một cuốn sách hay không và được tính như một accessor bên trong mô hình Author.
// Controller:
public function index()
{
$authors = Author::all();
return view('authors.index', compact('authors'));
}
// Blade file
@foreach($authors as $author)
<tr>
<td>{{ $author->name }}</td>
<td>{{ $author->is_active ? 'Yes' : 'No' }}</td>
</tr>
@endforeach
"is_active" được định nghĩa trong model Eloquent:
use Illuminate\Database\Eloquent\Casts\Attribute;
class Author extends Model
{
public function isActive(): Attribute
{
return Attribute::make(
get: fn () => $this->books->count() > 0,
);
}
}
Hãy xem Debugbar hiển thị những gì:
Đúng, chúng ta có thể giải quyết bằng eager loading các sách trong Controller. Nhưng trong trường hợp này, lời khuyên chung của tôi là tránh sử dụng các mối quan hệ trong accessor . Bởi vì accessor thường được sử dụng khi hiển thị dữ liệu và trong tương lai, người khác có thể sử dụng accessor này trong một số tệp Blade khác và bạn sẽ không kiểm soát được Controller đó trông như thế nào.
Nói cách khác, Accessor được cho là một phương pháp có thể tái sử dụng để định dạng dữ liệu, do đó bạn không kiểm soát được khi nào/cách thức dữ liệu sẽ được tái sử dụng. Trong trường hợp hiện tại của bạn, bạn có thể tránh truy vấn N+1, nhưng trong tương lai, người khác có thể không nghĩ đến nó.