Laravel 13 广播系统详解

广播系统允许在服务器端和客户端之间实现实时通信,是构建实时应用的核心组件。本文将深入探讨 Laravel 13 的广播系统。

配置

广播配置

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
// config/broadcasting.php
return [
'default' => env('BROADCAST_CONNECTION', 'null'),

'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'),
'encrypted' => true,
'host' => env('PUSHER_HOST'),
'port' => env('PUSHER_PORT', 443),
'scheme' => env('PUSHER_SCHEME', 'https'),
],
],

'ably' => [
'driver' => 'ably',
'key' => env('ABLY_KEY'),
],

'log' => [
'driver' => 'log',
],

'null' => [
'driver' => 'null',
],
],
];

启用广播

1
2
3
4
// config/app.php
'providers' => [
App\Providers\BroadcastServiceProvider::class,
],

定义广播事件

创建广播事件

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
36
37
38
39
40
41
42
43
44
45
46
47
<?php

namespace App\Events;

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

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

public function __construct(
public Order $order
) {}

public function broadcastOn(): array
{
return [
new PrivateChannel('user.'.$this->order->user_id),
];
}

public function broadcastAs(): string
{
return 'order.shipped';
}

public function broadcastWith(): array
{
return [
'id' => $this->order->id,
'tracking_number' => $this->order->tracking_number,
'status' => $this->order->status,
];
}

public function broadcastQueue(): string
{
return 'broadcasts';
}
}

频道类型

公共频道

1
2
3
4
5
6
public function broadcastOn(): array
{
return [
new Channel('orders'),
];
}

私有频道

1
2
3
4
5
6
public function broadcastOn(): array
{
return [
new PrivateChannel('user.'.$this->user->id),
];
}

存在频道

1
2
3
4
5
6
public function broadcastOn(): array
{
return [
new PresenceChannel('chat.'.$this->chat->id),
];
}

授权频道

定义频道路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// routes/channels.php
use Illuminate\Support\Facades\Broadcast;

Broadcast::channel('user.{id}', function ($user, $id) {
return (int) $user->id === (int) $id;
});

Broadcast::channel('order.{id}', function ($user, $id) {
return $user->orders()->where('id', $id)->exists();
});

Broadcast::channel('chat.{chatId}', function ($user, $chatId) {
if ($user->chats()->where('id', $chatId)->exists()) {
return [
'id' => $user->id,
'name' => $user->name,
];
}
});

频道类

1
php artisan make:channel OrderChannel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

namespace App\Broadcasting;

use App\Models\Order;
use App\Models\User;

class OrderChannel
{
public function join(User $user, int $orderId): bool
{
return $user->orders()->where('id', $orderId)->exists();
}
}

// 注册
Broadcast::channel('order.{id}', OrderChannel::class);

客户端订阅

Laravel Echo

1
2
3
4
5
6
7
8
9
10
11
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';

window.Pusher = Pusher;

window.Echo = new Echo({
broadcaster: 'pusher',
key: import.meta.env.VITE_PUSHER_APP_KEY,
cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
encrypted: true,
});

监听公共频道

1
2
3
4
Echo.channel('orders')
.listen('OrderShipped', (e) => {
console.log(e.order);
});

监听私有频道

1
2
3
4
5
6
7
Echo.private(`user.${userId}`)
.listen('OrderShipped', (e) => {
console.log(e.order);
})
.listen('OrderDelivered', (e) => {
console.log(e.order);
});

监听存在频道

1
2
3
4
5
6
7
8
9
10
11
12
13
Echo.join(`chat.${chatId}`)
.here((users) => {
console.log('Users in chat:', users);
})
.joining((user) => {
console.log('User joined:', user);
})
.leaving((user) => {
console.log('User left:', user);
})
.listen('MessageSent', (e) => {
console.log('New message:', e.message);
});

广播通知

广播通知

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
<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\BroadcastMessage;

class OrderShipped extends Notification
{
use Queueable;

public function toBroadcast($notifiable): BroadcastMessage
{
return new BroadcastMessage([
'order_id' => $this->order->id,
'message' => 'Your order has been shipped!',
]);
}

public function broadcastOn()
{
return new PrivateChannel('user.'.$this->notifiable->id);
}
}

监听通知

1
2
3
4
Echo.private(`user.${userId}`)
.notification((notification) => {
console.log(notification.message);
});

广播队列

队列广播

1
2
3
4
5
6
7
8
9
class OrderShipped implements ShouldBroadcast
{
use InteractsWithQueue;

public function broadcastQueue(): string
{
return 'broadcasts';
}
}

广播事件

分发广播事件

1
2
3
4
5
use App\Events\OrderShipped;

OrderShipped::dispatch($order);

event(new OrderShipped($order));

条件广播

1
2
3
4
5
6
7
class OrderShipped implements ShouldBroadcast
{
public function broadcastWhen(): bool
{
return $this->order->user->wantsNotifications();
}
}

存在频道高级用法

用户加入/离开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Echo.join(`chat.${chatId}`)
.here((users) => {
this.users = users;
})
.joining((user) => {
this.users.push(user);
this.notify(`${user.name} joined the chat`);
})
.leaving((user) => {
this.users = this.users.filter(u => u.id !== user.id);
this.notify(`${user.name} left the chat`);
})
.error((error) => {
console.error(error);
});

踢出用户

1
2
3
4
5
6
7
8
9
10
Broadcast::channel('chat.{chatId}', function ($user, $chatId) {
if ($user->isBannedFrom($chatId)) {
return false;
}

return [
'id' => $user->id,
'name' => $user->name,
];
});

测试广播

广播伪造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Illuminate\Support\Facades\Event;
use App\Events\OrderShipped;

public function test_order_shipped_broadcast(): void
{
Event::fake();

$order = Order::factory()->create();

OrderShipped::dispatch($order);

Event::assertDispatched(OrderShipped::class);
Event::assertDispatched(function (OrderShipped $event) use ($order) {
return $event->order->id === $order->id;
});
}

频道断言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Illuminate\Support\Facades\Broadcast;

public function test_channel_authorization(): void
{
$user = User::factory()->create();

$response = $this->actingAs($user)
->postJson('/broadcasting/auth', [
'socket_id' => 'test',
'channel_name' => "private-user.{$user->id}",
]);

$response->assertOk();
}

最佳实践

1. 使用队列广播

1
2
3
4
5
6
7
8
9
10
// 好的做法
class OrderShipped implements ShouldBroadcast
{
use InteractsWithQueue;
}

// 不好的做法
class OrderShipped implements ShouldBroadcastNow
{
}

2. 最小化广播数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 好的做法
public function broadcastWith(): array
{
return [
'id' => $this->order->id,
'status' => $this->order->status,
];
}

// 不好的做法
public function broadcastWith(): array
{
return $this->order->toArray();
}

3. 使用频道类

1
2
3
4
5
6
7
// 好的做法
Broadcast::channel('order.{id}', OrderChannel::class);

// 不好的做法
Broadcast::channel('order.{id}', function ($user, $id) {
return $user->orders()->where('id', $id)->exists();
});

总结

Laravel 13 的广播系统提供了强大的实时通信能力。通过合理使用公共频道、私有频道和存在频道,可以构建出各种实时应用场景。记住使用队列处理广播事件、最小化广播数据量、并使用频道类来组织授权逻辑。广播是构建现代 Web 应用的重要组件,掌握它对于开发现代实时应用至关重要。