广播系统
简介
在现代的 web 应用程序中, WebSockets 被用来实现实时、即时更新的用户接口。当服务器上的数据更新后,更新信息会通过 WebSocket 连接发送到客户端等待处理。相比于不停地轮询应用程序,这是一种更加可靠和高效的选择。
为了帮助你构建这类应用, Laravel 将通过 WebSocket 连接来使「广播」 事件 变得更加轻松。 广播 Laravel 事件允许你在服务端和客户端 JavaScript 应用程序间共享相同的事件名。
{tip} 在深入了解事件广播之前,请确认你已阅读所有关于 Laravel 事件和监听器 的文档。
配置
所有关于事件广播的配置都保存在 config/broadcasting.php
配置文件中。 Laravel 自带了几个广播驱动: Pusher 、 Redis , 和一个用于本地开发与调试的 log
驱动。另外,还有一个 null
驱动允许你完全关闭广播系统。每一个驱动的示例配置都可以在 config/broadcasting.php
配置文件中找到。
广播服务提供者
在对事件进行广播之前,你必须先注册 App\Providers\BroadcastServiceProvider
。对于一个新建的 Laravel 应用程序,你只需要在 config/app.php
配置文件的 providers
数组中取消对该提供者的注释即可。该提供者将允许你注册广播授权路由和回调。
CSRF 令牌
Laravel Echo 需要访问当前会话的 CSRF 令牌。你应当验证你的应用程序的 head
HTML 元素是否定义了包含 CSRF 令牌的 meta
标签:
<meta name="csrf-token" content="{{ csrf_token() }}">
对驱动的要求
Pusher
如果你使用 Pusher 来对事件进行广播,请用 Composer 包管理器来安装 Pusher PHP SDK :
composer require pusher/pusher-php-server "~3.0"
然后,你需要在 config/broadcasting.php
配置文件中配置你的 Pusher 证书。该文件中已经包含了一个 Pusher 示例配置,你可以快速地指定你的 Pusher key 、secret 和 application ID。 config/broadcasting.php
文件的 pusher
配置项同时也允许你指定 Pusher 支持的额外 options
,例如 cluster:
'options' => [
'cluster' => 'eu',
'encrypted' => true
],
当 Pusher 和 Laravel Echo 一起使用时,你应该在 resources/assets/js/bootstrap.js
文件中实例化 Echo 对象时指定 pusher
作为所需要的 broadcaster :
import Echo from "laravel-echo"
window.Pusher = require('pusher-js');
window.Echo = new Echo({
broadcaster: 'pusher',
key: 'your-pusher-key'
});
Redis
如果你使用 Redis 广播器,请安装 Predis 库:
composer require predis/predis
Redis 广播器会使用 Redis 的 发布/订阅 特性来广播消息;尽管如此,你仍需将它与能够从 Redis 接收消息的 WebSocket 服务器配对使用以便将消息广播到你的 WebSocket 频道上去。
当 Redis 广播器发布一个事件的时候,该事件会被发布到它指定的频道上去,传输的数据是一个采用 JSON 编码的字符串。该字符串包含了事件名、 data
数据和生成该事件 socket ID 的用户(如果可用的话)。
Socket.IO
如果你想将 Redis 广播器 和 Socket.IO 服务器进行配对,你需要在你的应用程序中引入 Socket.IO JavaScript 客户端库。你可以通过 NPM 包管理器进行安装:
npm install --save socket.io-client
然后,你需要在实例化 Echo 时指定 socket.io
连接器和 host
。
import Echo from "laravel-echo"
window.io = require('socket.io-client');
window.Echo = new Echo({
broadcaster: 'socket.io',
host: window.location.hostname + ':6001'
});
最后,你需要运行一个与 Laravel 兼容的 Socket.IO 服务器。 Laravel 官方并没有内置 Socket.IO 服务器实现;不过,可以选择一个由社区驱动维护的项目 tlaverdure/laravel-echo-server ,目前托管在 GitHub 。
对队列的要求
在开始广播事件之前,你还需要配置和运行 队列监听器 。所有的事件广播都是通过队列任务来完成的,因此应用程序的响应时间不会受到明显影响。
概念综述
Laravel 的事件广播允许你使用基于驱动的 WebSockets 将服务端的 Laravel 事件广播到客户端的 JavaScript 应用程序。当前的 Laravel 自带了 Pusher 和 Redis 驱动。通过使用 Laravel Echo 的 Javascript 包,我们可以很方便地在客户端消费事件。
事件通过「频道」来广播,这些频道可以被指定为公开或私有的。任何访客都可以不经授权或认证订阅一个公开频道;然而,如果想要订阅一个私有频道,那么该用户必须通过认证,并获得该频道的授权。
使用示例程序
在深入了解事件广播的每个组件之前,让我们先用一个电子商务网站作为例子来概览一下。我们不会讨论配置 Pusher 或者 Laravel Echo 的细节,这些会在本文档的其它章节里详细讨论。
在我们的应用程序中,我们假设有一个允许用户查看订单配送状态的页面。有一个 ShippingStatusUpdated
事件会在配送状态更新时被触发:
event(new ShippingStatusUpdated($update));
ShouldBroadcast 接口
当用户在查看自己的订单时,我们不希望他们必须通过刷新页面才能看到状态更新。我们希望一旦有更新时就主动将更新信息广播到客户端。所以,我们必须标记 ShippingStatusUpdated
事件实现 ShouldBroadcast
接口。这会让 Laravel 在事件被触发时广播该事件:
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class ShippingStatusUpdated implements ShouldBroadcast
{
/**
* 有关配送状态更新的信息。
*
* @var string
*/
public $update;
}
ShouldBroadcast
接口要求事件定义一个 broadcastOn
方法。该方法负责指定事件被广播到哪些频道。在(通过 Artisan 命令)生成的事件类中,一个空的 broadcastOn
方法已经被预定义好了,所以我们只需要完成其细节即可。我们希望只有订单的创建者能够看到状态的更新,所以我们要把该事件广播到与这个订单绑定的私有频道上去:
/**
* 获取事件应该广播的频道。
*
* @return array
*/
public function broadcastOn()
{
return new PrivateChannel('order.'.$this->update->order_id);
}
授权频道
记住,用户只有在被授权之后才能监听私有频道。我们可以在 routes/channels.php
文件中定义频道的授权规则。在本例中,我们需要对视图监听私有 order.1
频道的所有用户进行验证,确保只有订单真正的创建者才能监听:
Broadcast::channel('order.{orderId}', function ($user, $orderId) {
return $user->id === Order::findOrNew($orderId)->user_id;
});
channel
方法接收两个参数:频道名称和一个回调函数,该回调通过返回 true
或者 false
来表示用户是否被授权监听该频道。
所有的授权回调接收当前被认证的用户作为第一个参数,任何额外的通配符参数作为后续参数。在本例中,我们使用 {orderId}
占位符来表示频道名称的 「ID」 部分是通配符。
对事件广播进行监听
接下来,就只剩下在 JavaScript 应用程序中监听事件了。我们可以通过 Laravel Echo 来实现。首先,我们使用 private
方法来订阅私有频道。然后,使用 listen
方法来监听 ShippingStatusUpdated
事件。默认情况下,事件的所有公有属性会被包括在广播事件中:
Echo.private(`order.${orderId}`)
.listen('ShippingStatusUpdated', (e) => {
console.log(e.update);
});
定义广播事件
要告知 Laravel 一个给定的事件需要广播,只需要在事件类中实现 Illuminate\Contracts\Broadcasting\ShouldBroadcast
接口即可。该接口已被导入到所有由框架生成的事件类中,所以你可以很方便地将它添加到你自己的事件中。
ShouldBroadcast
接口要求你实现一个方法: broadcastOn
。 该方法返回一个频道或者一个频道数组,事件会被广播到这些频道。这些频道必须是 Channel
、PrivateChannel
或者 PresenceChannel
的实例。 Channel
代表任何用户都可以订阅的公开频道, 而 PrivateChannels
和 PresenceChannels
则代表需要 频道授权 的私有频道:
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class ServerCreated implements ShouldBroadcast
{
use SerializesModels;
public $user;
/**
* 创建一个新的事件实例。
*
* @return void
*/
public function __construct(User $user)
{
$this->user = $user;
}
/**
*获得事件广播的频道。
*
* @return Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('user.'.$this->user->id);
}
}
然后,你只需要像你平时那样 触发事件 。一旦事件被触发,一个 队列任务 会自动广播事件到你指定的广播驱动上。
广播名称
Laravel 默认会使用事件的类名作为广播名称来广播事件。不过,你也可以在事件类中定义一个 broadcastAs
方法来自定义广播名称:
/**
* 事件的广播名称。
*
* @return string
*/
public function broadcastAs()
{
return 'server.created';
}
如果你使用了 broadcastAs
方法来自定义广播名称,你应当确保在你注册监听器时加上一个 .
的前缀。这将指示 Echo 不要在事件之前添加应用程序的命名空间:
.listen('.server.created', function (e) {
....
});
广播数据
当一个事件被广播时,其所有的 public
属性都会自动序列化并作为事件有效载荷进行广播,这允许你在 JavaScript 应用程序中访问到事件所有的公有数据。举个例子,如果你的事件有一个单独的包含了一个 Eloquent 模型的公有 $user
属性,那么事件的广播有效载荷将会是:
{
"user": {
"id": 1,
"name": "Patrick Stewart"
...
}
}
不过,如果你想更细粒度地控制你的广播有效载荷,你可以向你的事件中添加一个 broadcastWith
方法。这个方法会返回一个你想要作为事件有效载荷进行广播的数据数组:
/**
* 指定广播数据。
*
* @return array
*/
public function broadcastWith()
{
return ['id' => $this->user->id];
}
广播队列
默认情况下,每一个广播事件都会被推送到在 queue.php
配置文件中指定的默认队列连接相应的默认队列中。你可以在事件类中定义一个 broadcastQueue
属性来自定义广播器所使用的队列。该属性需要你指定广播时你想要用的队列名称:
/**
* 事件被推送到的队列名称。
*
* @var string
*/
public $broadcastQueue = 'your-queue-name';
如果你想使用 sync
队列而不是默认队列驱动来广播事件,你可以实现 ShouldBroadcastNow
接口而不是 ShouldBroadcast
:
<?php
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
class ShippingStatusUpdated implements ShouldBroadcastNow
{
//
}
广播条件
有时,你想在给定条件为 true 的情况下才广播你的事件。你可以通过在事件类中添加一个 broadcastWhen
方法来定义这些条件:
/**
* 判定事件是否可以广播。
*
* @return bool
*/
public function broadcastWhen()
{
return $this->value > 100;
}
授权频道
对于私有频道,用户只有被授权之后才能监听。实现过程是用户向你的 Laravel 应用程序发起一个携带频道名称的 HTTP 请求,由你的应用程序判断该用户是否能够监听该频道。在使用 Laravel Echo 时,授权订阅私有频道的 HTTP 请求会自动发送;尽管如此,你仍需定义相应的路由来响应这些请求。
定义授权路由
幸运的是,在 Laravel 中我们可以很容易地定义路由来响应频道授权请求。在 Laravel 自带的 BroadcastServiceProvider
中,你可以看到对 Broadcast::routes
方法的调用。该方法会注册 /broadcasting/auth
路由来处理授权请求:
Broadcast::routes();
Broadcast::routes
方法会自动将它的路由置入 web
中间件组中;不过,如果你想自定义指定的属性,你可以向该方法传递一个路由属性数组:
Broadcast::routes($attributes);
定义授权回调
接下来,我们需要定义真正用于处理频道授权的逻辑。该逻辑在应用程序自带的 routes/channels.php
文件中完成。在这个文件中,你可以使用 Broadcast::channel
方法来注册频道授权回调:
Broadcast::channel('order.{orderId}', function ($user, $orderId) {
return $user->id === Order::findOrNew($orderId)->user_id;
});
channel
方法接收两个参数:频道名称和一个回调函数,该回调通过返回 true
或者 false
来表示用户是否被授权监听该频道。
所有的授权回调接收当前认证用户作为第一个参数,任何额外的通配符参数作为后续参数。在本例中,我们使用 {orderId}
占位符来表示频道名称的 「ID」 部分是通配符。
授权回调模型绑定
就像 HTTP 路由一样,频道路由也可以利用显式或隐式 路由模型绑定 。例如,你可以请求接收一个真正的 Order
模型实例,而不是字符串或数字类型的 order ID:
use App\Order;
Broadcast::channel('order.{order}', function ($user, Order $order) {
return $user->id === $order->user_id;
});
定义频道类
如果你的应用程序消耗了许多不同的频道,你的 routes/channels.php
文件可能会变得很庞大。所以,你可以使用频道类来代替使用闭包授权频道。要生成一个频道类,请使用 make:channel
Artisan 命令。该命令会在 App/Broadcasting
目录中放置一个新的频道类。
php artisan make:channel OrderChannel
接下来,在你的 routes/channels.php
文件中注册你的频道:
use App\Broadcasting\OrderChannel;
Broadcast::channel('order.{order}', OrderChannel::class);
最后,你可以将频道的授权逻辑放入频道类的 join
方法中。该 join
方法将保存你通常放置在频道授权闭包中的相同逻辑。当然,你也可以利用频道模型绑定:
<?php
namespace App\Broadcasting;
use App\User;
use App\Order;
class OrderChannel
{
/**
* 创建一个新的频道实例。
*
* @return void
*/
public function __construct()
{
//
}
/**
* 认证用户的频道访问权限。
*
* @param \App\User $user
* @param \App\Order $order
* @return array|bool
*/
public function join(User $user, Order $order)
{
return $user->id === $order->user_id;
}
}
{tip} 就像 Laravel 中的很多其它类,频道类会通过 服务容器 自动解析。因此,你可以在频道类的构造函数中对其进行所需依赖项的类型提示。
广播事件
定义好一个事件且将其标记实现 ShouldBroadcast
接口之后,你所要做的仅仅是通过 event
函数来触发该事件。事件分发器会识别出标记了实现 ShouldBroadcast
接口的事件,并将其推送到队列中进行广播:
event(new ShippingStatusUpdated($update));
只广播给他人
当创建一个会用到事件广播的应用程序时,你可以使用 broadcast
函数来代替 event
。和 event
函数一样, broadcast
函数将事件分发到服务端监听器:
broadcast(new ShippingStatusUpdated($update));
不过, broadcast
函数还有一个允许你将当前用户排除在广播接收者之外的 toOthers
方法:
broadcast(new ShippingStatusUpdated($update))->toOthers();
为了更好地理解什么时候使用 toOthers
方法,让我们假设有一个任务列表的应用程序,用户可以通过输入任务名来新建任务。要新建任务,你的应用程序需要发起一个请求到一个 /task
路由,该路由会广播任务的创建,并返回新任务的 JSON 响应。当你的 JavaScript 应用程序从路由收到响应后,它会直接将新任务插入到任务列表中,就像这样:
axios.post('/task', task)
.then((response) => {
this.tasks.push(respo
});
然而,别忘了,我们还广播了任务的创建。如果你的 JavaScript 应用程序正在监听该事件以便添加任务至任务列表,任务列表中将出现重复的任务:一个来自路由响应,另一个来自广播。你可以通过使用 toOthers
方法告知广播器不要将事件广播到当前用户来解决这个问题。
{note} 为了能调用
toOthers
方法,你的事件必须使用Illuminate\Broadcasting\InteractsWithSockets
trait 。
配置
当你初始化 Laravel Echo 实例的时候,一个套接字 ID 会被分配到该连接。如果你使用了 Vue 和 Axios ,该套接字 ID 会自动地以 X-Socket-ID
头的方式添加到每一个传出请求中。那么,当你调用 toOthers
方法时,Laravel 会从请求头中取出套接字 ID ,并告知广播器不要广播任何消息到带有这个套接字 ID 的连接上。
你如果你没有使用 Vue 和 Axios ,则需要手动配置 JavaScript 应用程序来发送 X-Socket-ID
请求头。你可以用 Echo.socketId
方法来获取套接字 ID :
var socketId = Echo.socketId();
接收广播
安装 Laravel Echo
Laravel Echo 是一个 JavaScript 库,有了这个库之后,订阅频道监听 Laravel 广播的事件变得非常容易。你可以通过 NPM 包管理器来安装 Echo 。在本例中,因为我们会使用 Pusher 广播器,所以我们也会安装 pusher-js
包:
npm install --save laravel-echo pusher-js
安装好 Echo 之后,你就可以在应用程序的 JavaScript 中创建一个全新的 Echo 实例。做这件事的一个理想的地方是在 Laravel 框架自带的 resources/js/bootstrap.js
文件的底部:
import Echo from "laravel-echo"
window.Echo = new Echo({
broadcaster: 'pusher',
key: 'your-pusher-key'
});
当你使用 pusher
连接器来创建一个 Echo 实例的时候,还可以指定 cluster
以及连接是否需要加密:
window.Echo = new Echo({
broadcaster: 'pusher',
key: 'your-pusher-key',
cluster: 'eu',
encrypted: true
});
对事件进行监听
安装并实例化 Echo 之后, 你就可以开始监听事件广播了。首先,使用 channel
方法获取一个频道实例,然后调用 listen
方法来监听指定的事件:
Echo.channel('orders')
.listen('OrderShipped', (e) => {
console.log(e.order.name);
});
如果你想监听私有频道上的事件,请使用 private
方法。你可以通过链式调用 listen
方法来监听单个频道上的多个事件:
Echo.private('orders')
.listen(...)
.listen(...)
.listen(...);
退出频道
如果想退出频道,可以在你的 Echo 实例上调用 leave
方法:
Echo.leave('orders');
命名空间
你可能已经注意到在上面的例子中,我们并没有为事件类指定完整的命名空间。这是因为 Echo 会默认事件都在 App\Events
命名空间下。不过,你可以在实例化 Echo 时传递一个 namespace
配置项来指定根命名空间:
window.Echo = new Echo({
broadcaster: 'pusher',
key: 'your-pusher-key',
namespace: 'App.Other.Namespace'
});
另外,你可以在使用 Echo 订阅事件的时候为事件类加上 .
前缀。这样就可以指定完全限定名称的类名了:
Echo.channel('orders')
.listen('.Namespace.Event.Class', (e) => {
//
});
Presence 频道
Presence 频道构建在私有频道的安全性基础之上,并提供了额外的特性:获知谁订阅了该频道。这一点使构建强大的,协同的应用变得非常容易,比如一个用户在浏览页面时,通知其他正在浏览相同页面的用户。
授权 Presence 频道
所有的 presence 频道也是私有频道;因此,用户必须被 授权之后才能访问 。不过,在给 presence 频道定义授权回调函数时,如果一个用户已经加入了该频道,那么不应该返回 true
,而应该返回一个关于该用户信息的数组。
由授权回调函数返回的数据能够在你的 JavaScript 应用程序中被 presence 频道事件监听器所使用。如果用户没有被授权加入该 presence 频道,那么你应该返回 false
或者 null
:
Broadcast::channel('chat.{roomId}', function ($user, $roomId) {
if ($user->canJoinRoom($roomId)) {
return ['id' => $user->id, 'name' => $user->name];
}
});
加入 Presence 频道
你可以使用 Echo 的 join
方法来加入 presence 频道。 join
方法会返回一个实现了 PresenceChannel
的对象,通过暴露 listen
方法,允许你订阅 here
、 joining
和 leaving
事件。
Echo.join(`chat.${roomId}`)
.here((users) => {
//
})
.joining((user) => {
console.log(user.name);
})
.leaving((user) => {
console.log(user.name);
});
here
回调函数会在你成功加入频道后被立即执行,并接收一个包含其他所有当前订阅该频道的用户的用户信息数组 。 joining
方法会在新用户加入频道时被执行,而 leaving
方法会在用户退出频道时被执行。
广播到 Presence 频道
Presence 频道可以像公开和私有频道一样接收事件。使用一个聊天室的例子,我们可能想把 NewMessage
事件广播到聊天室的 presence 频道。要实现它,我们将从事件的 broadcastOn
方法中返回一个 PresenceChannel
实例:
/**
* 获得事件广播的频道。
*
* @return Channel|array
*/
public function broadcastOn()
{
return new PresenceChannel('room.'.$this->message->room_id);
}
就像公开或私有事件, presence 频道事件也可以使用 broadcast
函数来广播。同样的,你也可以使用 toOthers
方法将当前用户排除在广播接收者之外:
broadcast(new NewMessage($message));
broadcast(new NewMessage($message))->toOthers();
你可以通过 Echo 的 listen
方法来监听 join 事件:
Echo.join(`chat.${roomId}`)
.here(...)
.joining(...)
.leaving(...)
.listen('NewMessage', (e) => {
//
});
客户端事件
{tip} 使用 Pusher 时,如果要发送客户端事件,你必须在 应用后台 的「应用设置」部分启用「客户端事件」选项。
有时,你可能希望广播一个事件给其它已经连接的客户端,但不通知你的 Laravel 应用程序。这在处理「输入中」这类事情的通知时尤其有用,比如提醒你应用的用户,另一个用户正在给定屏幕上输入信息。
你可以使用 Echo 的 whisper
方法来广播客户端事件:
Echo.private('chat')
.whisper('typing', {
name: this.user.name
});
你可以使用 listenForWhisper
方法来监听客户端事件:
Echo.private('chat')
.listenForWhisper('typing', (e) => {
console.log(e.name);
});
消息通知
通过与将事件广播与 消息通知 配对,你的 JavaScript 应用程序可以在不刷新页面的情况下接收新的消息通知。在此之前,请确保你已经读过了如何使用 广播通知频道 的文档。
配置好使用广播频道的消息通知后,你可以使用 Echo 的 notification
方法来监听广播事件。谨记,频道名称应该和接收消息通知的实体类名相匹配:
Echo.private(`App.User.${userId}`)
.notification((notification) => {
console.log(notification.type);
});
在本例中,所有通过 broadcast
频道发送到 App\User
实例的消息通知都会被回调接收。一个针对 App.User.{id}
频道的授权回调函数已经包含在 Laravel 框架内置的 BroadcastServiceProvider
中了。
本文章首发在 LearnKu.com 网站上。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接 我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。