广播系统

简介

在现代的 web 应用程序中, WebSockets 被用来实现实时、即时更新的用户接口。当服务器上的数据更新后,更新信息会通过 WebSocket 连接发送到客户端等待处理。相比于不停地轮询应用程序,这是一种更加可靠和高效的选择。

为了帮助你构建这类应用, Laravel 将通过 WebSocket 连接来使「广播」 事件 变得更加轻松。 广播 Laravel 事件允许你在服务端和客户端 JavaScript 应用程序间共享相同的事件名。

{注} 在深入了解事件广播之前,请确认你已阅读所有关于 Laravel 事件和监听器 的文档。

配置

所有关于事件广播的配置都保存在 config/broadcasting.php 配置文件中。 Laravel 自带了几个广播驱动: PusherRedis , 和一个用于本地开发与调试的 log 驱动。另外,还有一个 null 驱动允许你完全关闭广播系统。每一个驱动的示例配置都可以在 config/broadcasting.php 配置文件中找到。

广播服务提供者

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

CSRF 令牌

Laravel Echo 需要访问当前会话的 CSRF 令牌。你应当验证你的应用程序的 head HTML 元素是否定义了包含 CSRF 令牌的 meta 标签:

  1. <meta name="csrf-token" content="{{ csrf_token() }}">

对驱动的要求

Pusher

如果你使用 Pusher 来对事件进行广播,请用 Composer 包管理器来安装 Pusher PHP SDK :

  1. 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:

  1. 'options' => [
  2. 'cluster' => 'eu',
  3. 'encrypted' => true
  4. ],

当 Pusher 和 Laravel Echo 一起使用时,你应该在 resources/assets/js/bootstrap.js 文件中实例化 Echo 对象时指定 pusher 作为所需要的 broadcaster :

  1. import Echo from "laravel-echo"
  2. window.Pusher = require('pusher-js');
  3. window.Echo = new Echo({
  4. broadcaster: 'pusher',
  5. key: 'your-pusher-key'
  6. });

Redis

如果你使用 Redis 广播器,请安装 Predis 库:

  1. composer require predis/predis

Redis 广播器会使用 Redis 的 发布/订阅 特性来广播消息;尽管如此,你仍需将它与能够从 Redis 接收消息的 WebSocket 服务器配对使用以便将消息广播到你的 WebSocket 频道上去。

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

Socket.IO

如果你想将 Redis 广播器 和 Socket.IO 服务器进行配对,你需要在你的应用程序中引入 Socket.IO JavaScript 客户端库。你可以通过 NPM 包管理器进行安装:

  1. npm install --save socket.io-client

然后,你需要在实例化 Echo 时指定 socket.io 连接器和 host

  1. import Echo from "laravel-echo"
  2. window.io = require('socket.io-client');
  3. window.Echo = new Echo({
  4. broadcaster: 'socket.io',
  5. host: window.location.hostname + ':6001'
  6. });

最后,你需要运行一个与 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 事件会在配送状态更新时被触发:

  1. event(new ShippingStatusUpdated($update));

ShouldBroadcast 接口

当用户在查看自己的订单时,我们不希望他们必须通过刷新页面才能看到状态更新。我们希望一旦有更新时就主动将更新信息广播到客户端。所以,我们必须标记 ShippingStatusUpdated 事件实现 ShouldBroadcast 接口。这会让 Laravel 在事件被触发时广播该事件:

  1. <?php
  2. namespace App\Events;
  3. use Illuminate\Broadcasting\Channel;
  4. use Illuminate\Queue\SerializesModels;
  5. use Illuminate\Broadcasting\PrivateChannel;
  6. use Illuminate\Broadcasting\PresenceChannel;
  7. use Illuminate\Broadcasting\InteractsWithSockets;
  8. use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
  9. class ShippingStatusUpdated implements ShouldBroadcast
  10. {
  11. /**
  12. * 有关配送状态更新的信息。
  13. *
  14. * @var string
  15. */
  16. public $update;
  17. }

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 。 该方法返回一个频道或者一个频道数组,事件会被广播到这些频道。这些频道必须是 ChannelPrivateChannel 或者 PresenceChannel 的实例。 Channel 代表任何用户都可以订阅的公开频道, 而 PrivateChannelsPresenceChannels 则代表需要 频道授权 的私有频道:

<?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);

自定义授权端点

默认情况下,Echo 将使用 /broadcasting/auth 端点来授权频道访问。 但是,您可以通过将 authEndpoint 配置选项传递给 Echo 实例来指定自己的授权端点:

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: 'your-pusher-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 模型实例,而不是字符串或数字类型的 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(respo
    });

然而,别忘了,我们还广播了任务的创建。如果你的 JavaScript 应用程序正在监听该事件以便添加任务至任务列表,任务列表中将出现重复的任务:一个来自路由响应,另一个来自广播。你可以通过使用 toOthers 方法告知广播器不要将事件广播到当前用户来解决这个问题。

{注} 为了能调用 toOthers 方法,你的事件必须使用 Illuminate\Broadcasting\InteractsWithSockets trait 。

配置

当你初始化 Laravel Echo 实例的时候,一个套接字 ID 会被分配到该连接。如果你使用了 VueAxios ,该套接字 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 使用的 Pusher 或 Socket.io 客户端实例,您可以通过 client 配置选项将其传递给Echo:

const client = require('pusher-js');

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: 'your-pusher-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 命名空间下。不过,你可以在实例化 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 方法,允许你订阅 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 时,如果要发送客户端事件,你必须在 应用后台 的「应用设置」部分启用「客户端事件」选项。

有时,你可能希望广播一个事件给其它已经连接的客户端,但不通知你的 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 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。