前言

在現代 Web 應用中,即時互動功能已成為提升使用者體驗的關鍵。從即時通知、聊天室到協作編輯,WebSocket 技術為伺服器與客戶端之間的雙向通訊提供了高效的解決方案。本文將深度解析如何在 Laravel 框架中,利用 laravel-websockets 套件實現強大的即時功能,並提供常見使用場景的詳細實作指南。

WebSocket 基礎與 laravel-websockets 設定

WebSocket vs. HTTP Polling

在 WebSocket 出現之前,實現即時通訊通常依賴 HTTP 輪詢(Polling),但這種方式效率低下且浪費資源。WebSocket 提供了一個持久性的單一 TCP 連線,允許伺服器和客戶端隨時進行雙向數據傳輸。

sequenceDiagram
    participant Client
    participant Server

    Note over Client, Server: HTTP 長輪詢 (Long Polling)
    Client->>Server: Request
    Server-->>Client: No updates, wait...
    Server->>Client: Data available, Response
    Client->>Server: New Request (cycle repeats)

    Note over Client, Server: WebSocket
    Client->>Server: Upgrade Request (Handshake)
    Server-->>Client: Upgrade Response (Connection established)
    loop Bi-directional Communication
        Server->>Client: Push Data
        Client->>Server: Send Data
    end

laravel-websockets 套件安裝與設定

laravel-websockets 是一個基於 Ratchet 的純 PHP WebSocket 伺服器,它與 Laravel Broadcasting 系統無縫整合,並提供了 Pusher API 的完整替代方案。

步驟一:安裝套件

1
composer require beyondcode/laravel-websockets

步驟二:發布設定檔與遷移檔案

1
2
3
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="migrations"
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="config"
php artisan migrate

步驟三:設定 .envconfig/broadcasting.php

首先,在 config/broadcasting.php 中設定 Pusher 連線:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// config/broadcasting.php
'connections' => [
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'host' => env('PUSHER_HOST', '127.0.0.1'),
'port' => env('PUSHER_PORT', 6001),
'scheme' => env('PUSHER_SCHEME', 'http'),
'encrypted' => false, // for local development
'useTLS' => env('PUSHER_SCHEME') === 'https',
],
],
// ...
],

接著,更新 .env 檔案:

1
2
3
4
5
6
7
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=my-app-id
PUSHER_APP_KEY=my-app-key
PUSHER_APP_SECRET=my-app-secret
PUSHER_HOST=127.0.0.1
PUSHER_PORT=6001
PUSHER_SCHEME=http

步驟四:設定前端 Laravel Echo

安裝 Echo 和 Pusher 客戶端:

1
npm install --save-dev laravel-echo pusher-js

resources/js/bootstrap.js 中設定 Echo:

1
2
3
4
5
6
7
8
9
10
11
12
13
import Echo from 'laravel-echo';

window.Pusher = require('pusher-js');

window.Echo = new Echo({
broadcaster: 'pusher',
key: process.env.MIX_PUSHER_APP_KEY,
cluster: process.env.MIX_PUSHER_APP_CLUSTER,
wsHost: window.location.hostname,
wsPort: 6001,
forceTLS: false,
disableStats: true,
});

核心運作機制:事件與頻道

Laravel 的即時通訊基於事件廣播頻道訂閱機制。

graph TD
    A[後端觸發事件] --> B[Laravel Event System]
    B --> C{ShouldBroadcast}
    C -->|Yes| D[Dispatch to Queue]
    D --> E[Broadcast Driver]
    E --> F[WebSocket Server]
    F --> G[推送至指定頻道]
    
    subgraph "前端"
        H[Laravel Echo] --> I[訂閱頻道]
        I --> J[監聽事件]
        J --> K[更新 UI]
    end
    
    G --> I

頻道類型

  1. 公開頻道 (Public Channels):無需授權,任何人都可以訂閱。適合廣播公開資訊。
  2. 私有頻道 (Private Channels):需要授權才能訂閱。適合一對一的通知或聊天。
  3. 存在頻道 (Presence Channels):基於私有頻道,但額外提供訂閱者列表,可用於顯示「誰在線上」。

場景一:即時通知系統

這是最常見的 WebSocket 應用,例如當有新訂單或新訊息時,即時通知使用者。

步驟一:建立事件

1
php artisan make:event NewNotificationEvent

步驟二:實作事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// app/Events/NewNotificationEvent.php
namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class NewNotificationEvent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;

public $message;
private $userId;

public function __construct($userId, $message)
{
$this->userId = $userId;
$this->message = $message;
}

// 定義廣播頻道 (私有頻道)
public function broadcastOn()
{
return new PrivateChannel('notifications.' . $this->userId);
}

// 自訂廣播事件名稱
public function broadcastAs()
{
return 'new.notification';
}
}

步驟三:設定頻道授權

routes/channels.php 中定義授權邏輯:

1
2
3
4
5
6
7
// routes/channels.php
use Illuminate\Support\Facades\Broadcast;

Broadcast::channel('notifications.{userId}', function ($user, $userId) {
// 只有登入使用者自己可以監聽自己的通知頻道
return (int) $user->id === (int) $userId;
});

步驟四:觸發事件

在控制器或服務中觸發事件:

1
2
3
4
5
6
use App\Events\NewNotificationEvent;

// 向 User ID 為 1 的使用者發送通知
$userId = 1;
$message = '您有一筆新訂單!';
event(new NewNotificationEvent($userId, $message));

步驟五:前端監聽事件

1
2
3
4
5
6
7
8
9
// 假設 userId 已在前端可用
const userId = 1;

Echo.private(`notifications.${userId}`)
.listen('.new.notification', (e) => {
console.log(e.message);
// 在此處更新 UI,例如顯示一個通知彈窗
alert('新通知:' + e.message);
});

場景二:即時聊天室

聊天室是 WebSocket 的典型應用,我們將使用存在頻道來實現。

步驟一:建立聊天事件

1
php artisan make:event NewChatMessage

步驟二:實作事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// app/Events/NewChatMessage.php
namespace App\Events;

use App\Models\User;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class NewChatMessage implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;

public $message;
public $user;
private $chatRoomId;

public function __construct($chatRoomId, User $user, $message)
{
$this->chatRoomId = $chatRoomId;
$this->user = $user;
$this->message = $message;
}

public function broadcastOn()
{
return new PresenceChannel('chat.' . $this->chatRoomId);
}
}

步驟三:設定頻道授權

1
2
3
4
5
6
7
// routes/channels.php
Broadcast::channel('chat.{chatRoomId}', function ($user, $chatRoomId) {
// 假設有一個 ChatRoom 模型和 user_is_member 方法
if ($user->canJoinChatRoom($chatRoomId)) {
return ['id' => $user->id, 'name' => $user->name];
}
});

步驟四:觸發事件

1
2
3
4
5
6
7
8
9
10
11
12
13
// 在控制器中處理發送訊息的請求
public function sendMessage(Request $request, $chatRoomId)
{
$user = $request->user();
$message = $request->input('message');

// 儲存訊息到資料庫...

// 廣播事件
broadcast(new NewChatMessage($chatRoomId, $user, $message))->toOthers();

return response()->json(['status' => 'Message sent!']);
}

注意->toOthers() 方法可以避免訊息發送者自己收到廣播。

步驟五:前端監聽與互動

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const chatRoomId = 123;

Echo.join(`chat.${chatRoomId}`)
.here((users) => {
// 當前在頻道中的使用者列表
console.log('目前在線:', users);
// 更新在線使用者列表 UI
})
.joining((user) => {
// 新使用者加入
console.log(user.name, '加入了聊天室');
})
.leaving((user) => {
// 使用者離開
console.log(user.name, '離開了聊天室');
})
.listen('NewChatMessage', (e) => {
// 監聽到新訊息
console.log(e.user.name + ': ' + e.message);
// 將新訊息附加到聊天視窗
})
.error((error) => {
console.error('頻道授權失敗:', error);
});

場景三:即時儀表板更新

對於需要即時監控數據的儀表板(如銷售額、網站流量),WebSocket 是絕佳選擇。

步驟一:建立事件

1
php artisan make:event DashboardUpdate

步驟二:實作事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// app/Events/DashboardUpdate.php
namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
// ... 其他 use

class DashboardUpdate implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;

public $data;

public function __construct($data)
{
$this->data = $data;
}

public function broadcastOn()
{
// 使用公開頻道,因為儀表板數據通常是公開的
return new Channel('dashboard-updates');
}
}

步驟三:觸發事件

當有新數據產生時(例如,一筆新訂單完成),觸發事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 在訂單服務中
class OrderService
{
public function completeOrder(Order $order)
{
// ... 處理訂單邏輯 ...

// 準備儀表板更新數據
$dashboardData = [
'total_sales' => Sales::sum('amount'),
'new_orders_count' => Order::whereDate('created_at', today())->count(),
];

// 廣播更新
broadcast(new DashboardUpdate($dashboardData));
}
}

步驟四:前端監聽

1
2
3
4
5
6
7
8
9
10
11
Echo.channel('dashboard-updates')
.listen('DashboardUpdate', (e) => {
console.log('儀表板數據更新:', e.data);
// 使用 e.data 更新儀表板上的圖表或數字
updateDashboard(e.data);
});

function updateDashboard(data) {
document.getElementById('total-sales').innerText = data.total_sales;
document.getElementById('new-orders-count').innerText = data.new_orders_count;
}

部署與維護

啟動 WebSocket 伺服器

在開發環境中,可以直接使用 Artisan 命令啟動:

1
php artisan websockets:serve

使用 Supervisor 維持伺服器運行

在生產環境中,需要一個進程管理器來確保 WebSocket 伺服器持續運行。

1
2
3
4
5
6
7
8
9
10
11
; /etc/supervisor/conf.d/laravel-websockets.conf

[program:laravel-websockets]
process_name=%(program_name)s_%(process_num)02d
command=php /path/to/your/project/artisan websockets:serve
autostart=true
autorestart=true
user=your-user
numprocs=1
redirect_stderr=true
stdout_logfile=/path/to/your/project/storage/logs/websockets.log

Nginx 反向代理設定

為了使用標準的 80 和 443 埠,並支援 SSL,需要設定反向代理。

1
2
3
4
5
6
7
8
9
10
11
12
13
server {
# ... 您的標準 server 設定 ...

location /app {
proxy_pass http://127.0.0.1:6001/app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

總結

Laravel Websockets 為開發即時應用提供了強大而優雅的解決方案。通過掌握事件廣播、頻道授權和前端 Echo 監聽的核心概念,您可以輕鬆實現從簡單的即時通知到複雜的互動式聊天室等多種功能。在實作時,務必根據場景選擇合適的頻道類型,並在生產環境中做好部署和維護,以確保系統的穩定性和安全性。

學習建議

  1. 從私有頻道開始:私有頻道是學習頻道授權的最佳起點。
  2. 善用儀表板laravel-websockets 套件自帶一個儀表板(預設路徑 /laravel-websockets),可以用於調試和監控。
  3. 注意非同步:廣播事件最好在佇列中執行,以避免阻塞使用者請求。在事件類別中實現 ShouldQueue 介面即可。