广播系统
简介
在现代的 web 应用程序中, WebSockets 被用来实现实时、即时更新的用户接口。当服务器上的数据更新后,更新信息会通过 WebSocket 连接发送到客户端等待处理。相比于不停地轮询应用程序,这是一种更加可靠和高效的选择。
为了帮助你构建这类应用, Laravel 将通过 WebSocket 连接来使「广播」 事件 变得更加轻松。 广播 Laravel 事件允许你在服务端和客户端 JavaScript 应用程序间共享相同的事件名。
{注} 在深入了解事件广播之前,请确认你已阅读所有关于 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 "~4.0"
然后,你需要在 config/broadcasting.php
配置文件中配置你的 Pusher 证书。该文件中已经包含了一个 Pusher 示例配置,你可以快速地指定你的 Pusher key
、secret
和 application ID
。 config/broadcasting.php
文件的 pusher
配置项同时也允许你指定 Pusher 支持的额外 options
,例如 cluster
:
'options' => [
'cluster' => 'eu',
'useTLS' => true
],
当 Channels
和 Laravel Echo
一起使用时,你应该在 resources/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-channels-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
{
/**
* Information about the shipping status update.
*
* @var string
*/
public $update;
}
ShouldBroadcast
接口要求事件定义一个 broadcastOn
方法。该方法负责指定事件被广播到哪些频道。在(通过 Artisan 命令)生成的事件类中,一个空的 broadcastOn
方法已经被预定义好了,所以我们只需要完成其细节即可。我们希望只有订单的创建者能够看到状态的更新,所以我们要把该事件广播到与这个订单绑定的私有频道上去:
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\PrivateChannel
*/
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
。 broadcastOn
方法应该返回事件应该广播的频道或频道数组。 该频道应该是 Channel
,PrivateChannel
,或者 PresenceChannel
的实例. 。Channel
的实例 代表任何用户可以订阅的公共频道, PrivateChannels
和 PresenceChannels
实列代表需要频道授权的私有频道:
<?php
namespace App\Events;
use App\User;
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);
自定义授权端点
通常, Echo 会使用 /broadcasting/auth
端点来授权频道访问。 但是,你可以通过将 authEndpoint
配置选项传递给 Echo 实例来指定自己的授权端点:
window.Echo = new Echo({
broadcaster: 'pusher',
key: 'your-pusher-channels-key',
authEndpoint: '/custom/endpoint/auth'
});
定义授权回调
接下来,我们需要定义实际执行频道授权的逻辑。这是在你的应用程序附带的 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
模型实列来替代接收一个字符串或者订单 ID 数字:
use App\Order;
Broadcast::channel('order.{order}', function ($user, Order $order) {
return $user->id === $order->user_id;
});
授权回调验证
私有和在线广播频道,通过应用程序默认的认证防护器来认证当前用户。如果用户未经过认证,则会自动拒绝频道授权,并且永远不会执行授权回调。 但是,你可以分配多个自定义防护器,以便在必要时对传入请求进行身份验证:
Broadcast::channel('channel', function() {
// ...
}, ['guards' => ['web', 'admin']]);
定义频道类
如果你的应用程序使用了很多不同的频道, 那么 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;
}
}
{提示} 和很多 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(response.data);
});
但是,请记住,我们还广播任务的创建。 如果你的 JavaScript 应用程序正在收听此事件以便将任务添加到任务列表,则列表中将出现重复的任务:一个来自端点,另一个来自广播。 你可以使用toOthers
方法来指示广播器不向当前用户广播事件。
{注意} 你的事件必须使用
Illuminate\Broadcasting\InteractsWithSockets
trait 才能调用toOthers
方法。
配置
初始化 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-js
包,因为我们将使用 Pusher Channels 广播器:
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-channels-key'
});
在创建使用pusher
连接器的 Echo 实例时,你还可以指定cluster
是否必须通过 TLS 进行连接(默认情况下,当forceTLS
为false
时,如果页面是通过 HTTP 加载的,则会生成非 TLS 连接或者作为 TLS 连接失败时的回调):
window.Echo = new Echo({
broadcaster: 'pusher',
key: 'your-pusher-channels-key',
cluster: 'eu',
forceTLS: true
});
使用现有客户端实例
如果你已经有 Echo 使用的 Pusher Channels 或 Socket.io 客户端实例,你可以通过 client
配置选项将它传递给 Echo:
const client = require('pusher-js');
window.Echo = new Echo({
broadcaster: 'pusher',
key: 'your-pusher-channels-key',
client: client
});
对事件进行监听
一旦安装并实例化了 Echo,就可以开始监听事件广播了。 首先,使用channel
方法检索频道的实例,然后调用listen
方法来监听指定的事件:
Echo.channel('orders')
.listen('OrderShipped', (e) => {
console.log(e.order.name);
});
如果想在私有频道上收听事件,请使用private
方法。 可以通过链式调用listen
方法,在单个频道上侦听多个事件:
Echo.private('orders')
.listen(...)
.listen(...)
.listen(...);
退出频道
要退出频道,你可以在 Echo 实例上调用 leaveChannel
方法:
Echo.leaveChannel('orders');
如果你想退出一个频道以及相关的私有和在线频道,你可以调用leave
方法:
Echo.leave('orders');
命名空间
你可能已经在上面的示例中,注意到我们没有为事件类指定完整的命名空间。 这是因为 Echo 会自动假设事件位于App\Events
命名空间中。 但是,你可以通过传递 namespace
配置选项来实例化 Echo 时配置根命名空间:
window.Echo = new Echo({
broadcaster: 'pusher',
key: 'your-pusher-channels-key',
namespace: 'App.Other.Namespace'
});
或者,你可以在使用 Echo 订阅事件类时使用 .
作为前缀。 这将允许你始终指定完全限定的类名:
Echo.channel('orders')
.listen('.Namespace\\Event\\Class', (e) => {
//
});
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 频道
加入Presence 频道,你可以使用 Echo 的join
方法。 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 频道事件。 与其他事件一样,你可以使用toOthers
方法排除当前用户接收广播:
broadcast(new NewMessage($message));
broadcast(new NewMessage($message))->toOthers();
你可以通过 Echo 的listen
方法监听 join 事件:
Echo.join(`chat.${roomId}`)
.here(...)
.joining(...)
.leaving(...)
.listen('NewMessage', (e) => {
//
});
客户端事件
有时,你可能希望将事件广播到其他连接的客户端,而根本不需要使用 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);
});
在此示例中,回调将接收通过 广播
频道发送到 App\User
实例的所有通知。 App.User.{id}
频道的频道授权回调包含在 Laravel 框架附带的默认BroadcastServiceProvider
中。