广播

简介

在很多现代化的 Web 应用中,WebSockets 被用来实现实时的、即时更新的用户接口。当服务器上的一些数据更新后,信息通常会通过 WebSocket 连接发送到客户端进行处理。相比于不停地轮询应用,WebSocket 提供了更加可靠和高效的替代方案。

为了帮助您建立此类应用,Laravel 通过 WebSocket 连接来使「广播」事件 更容易。广播事件允许您在服务端代码和客户端 JavaScript 应用之间共享相同的事件名称。

在深入了解事件广播之前,确保已阅读了所有 Laravel 事件和监听器 的文档。

配置

所有事件广播的配置信息都存储在 config/broadcasting.php 文件中。Laravel 支持几个开箱即用的广播驱动:PusherRedis 和用于本地开发与调试的 log 驱动。此外,还自带一个 null 驱动用于完全禁用广播。每个驱动的示例配置都包含在 config/broadcasting.php 配置文件中。

广播服务提供者

对事件进行广播前,必须先注册 App\Providers\BroadcastServiceProvider。新的 Laravel 应用中,只需要在 config/app.php 配置文件的 providers 数组中取消对此服务提供者的注释即可。该服务提供者允许您注册广播授权路由和回调。

CSRF 令牌

Laravel Echo 需要访问当前会话的 CSRF 令牌。因此要确保在应用的 HTML 的 head 元素中定义 meta 标签时包含了 CSRF 令牌:

<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/js/bootstrap.js 文件中实例化 Echo 对象时将 pusher 指定为要使用的广播器:

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 的「生产者/消费者」功能来广播信息;不过,您仍需将它和 WebSocket 服务器一起使用。WebSocket 服务器会从 Redis 接收信息,然后再将信息广播到 WebSocket 频道上。

当 Redis 广播器发布一个事件时,该事件会被发布到其指定频道名称的频道上去,传输的负载是一个使用 JSON 编码的字符串。该字符串包含了事件名称、data 负载和生成该事件 socket ID 的用户(如果可用)。

Socket.IO

如果想把 Redis 广播器和 Socket.IO 服务器一起使用,则需要将 Socket.IO JavaScript 客户端库引入到应用中。可以通过 NPM 包管理器安装:

npm install --save socket.io-client

接下来,使用 socket.io 连接器和 host 实例化 Echo:

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 服务器的实现;不过,可以使用一个由社区驱动维护并托管在 GitHub 上的 tlaverdure/laravel-echo-server

队列驱动前提

在广播事件之前,还需要配置并运行 队列监听器。所有事件广播都通过队列任务完成,因此应用的响应时间不会受到明显影响。

概念综述

Laravel 的事件广播允许您使用基于驱动的 WebSockets 将服务端的 Laravel 事件广播到客户端的 JavaScript 客户端应用。目前,Laravel 自带了 Pusher 和 Redis 驱动。使用 Laravel Echo JavaScript 包,可以很方便地在客户端消费事件。

事件通过「频道」进行广播,这些频道可以指定为公开的或者私有的。应用的任何访问者都可以订阅不需要认证或授权的公开频道;但是,如果要订阅一个私有频道,那么此用户必须进过认证并授权收听该频道。

使用示例应用

在深入了解事件广播的各组件之前,我们先以电子商务网站为例,对其有一个高层次的概览。此处不会讨论配置 PusherLaravel Echo 的细节,因为这些会在本文档的其它章节展开详细讨论。

假设在应用中,有一个让用户查看订单配送状态的页面。并假设应用更新配送状态时触发 ShippingStatusUpdated 事件:

event(new ShippingStatusUpdated($update));

ShouldBroadcast 接口

当用户查看自己的订单时,我们不希望他们必须要刷新页面才能看到状态更新。而是,一旦有更新时就主动将其广播给应用。因此,我们需要让 ShippingStatusUpdated 事件实现 ShouldBroadcast 接口。它会指示 Laravel 在事件触发时广播事件:

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 方法。此方法负责返回事件被广播的频道。一个空的方法已经在生成的事件类中定义了,因此我们只要填写具体内容。我们只希望订单的创建者可以查看状态更新,因此要把事件广播到与此订单绑定的私有频道:

/**
 * 获取事件应该广播的频道
 *
 * @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 方法接收两个参数:频道名称和回调,回调返回 truefalse 表明用户是否被授权监听该频道。

所有的授权回调接收当前认证用户作为其第一个参数,以及任何其它通配符参数作为后面的参数。在本示例中,我们使用 {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 接口要求实现方法:broadcastOnbroadcastOn 方法应该返回事件广播的频道或频道数组。频道是 ChannelPrivateChannelPresenceChannel 的实例。Channel 实例表示任何用户都可以订阅的公开频道,而 PrivateChannelsPresenceChannels 表示需要 频道授权 的私有频道。

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

use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;

class ShippingStatusUpdated implements ShouldBroadcastNow
{
    //
}

广播条件

有时希望仅当给定条件为真时才广播事件。可以通过添加事件类的 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 方法接收两个参数:频道名称和回调,回调返回 truefalse 表明用户是否被授权监听该频道。

所有的授权回调接收当前认证用户作为其第一个参数,以及任何其它通配符参数作为后面的参数。在本例中,我们使用 {orderId} 占位符来表示频道名称的「ID」部分是通配符。

授权回调模型绑定

就像 HTTP 路由一样,频道路由也可以使用显示或隐式 路由模型绑定。例如,除了接收字符串或数字类型的订单 ID 之外,还可以请求一个 Order 模型实例:

use App\Order;

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

定义频道类

如果应用被很多不同的频道消费,routes/channels.php 文件会变得笨重。除了使用闭包授权频道,还可以使用频道类。要生成频道类,使用 Artisan 命令 make:channel。此命令会在 App/Broadcasting 目录中生成新的频道类:

php artisan make:channel OrderChannel

然后,在 routes/channels.php 文件中注册频道:

use App\Broadcasting\OrderChannel;

Broadcast::channel('order.{order}', OrderChannel::class);

最后,将频道的授权逻辑放在频道类的 join 方法中。join 方法会存放和通常放在频道授权闭包中的相同的逻辑。当然,也可以使用频道模型绑定:

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 实例时,会为该连接指定 socket ID。如果使用 VueAxios,socket ID 会在 X-Socket-ID 头信息中自动添加给每个传出的请求。然后,调用 toOthers 方法,Laravel 从头信息中提取出 socket ID 并指示广播器不要广播给该 socket ID 的任何连接。

如果不使用 Vue 和 Axios,则需要手动配置 JavaScript 应用发送 X-Socket-ID 头信息。可以使用 Echo.socketId 方法获取 socket 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 频道

加入 Presence 频道,可以使用 Echo 的 join 方法。join 方法会返回一个 PresenceChannel 实现,并提供 listen 方法订阅 herejoiningleaving 事件。

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) => {
        //
    });

客户端事件

使用 Pusher 时,为了发送客户端事件,必须在 应用仪表盘 的「App Settings」中启用「Client Events」选项。

有时可能在广播事件给其它连接的客户端时,根本不用通知 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 中。