广播系统

简介

在现代的 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 "~4.0"

然后,你需要在 config/broadcasting.php 配置文件中配置你的 Pusher 证书。该文件中已经包含了一个 Pusher 示例配置,你可以快速地指定你的 Pusher keysecretapplication IDconfig/broadcasting.php 文件的 pusher配置项同时也允许你指定 Pusher 支持的额外 options ,例如 cluster

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

ChannelsLaravel Echo 一起使用时,你应该在 resources/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-channels-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. * Information about the shipping status update.
  13. *
  14. * @var string
  15. */
  16. public $update;
  17. }

ShouldBroadcast 接口要求事件定义一个 broadcastOn 方法。该方法负责指定事件被广播到哪些频道。在(通过 Artisan 命令)生成的事件类中,一个空的 broadcastOn 方法已经被预定义好了,所以我们只需要完成其细节即可。我们希望只有订单的创建者能够看到状态的更新,所以我们要把该事件广播到与这个订单绑定的私有频道上去:

  1. /**
  2. * Get the channels the event should broadcast on.
  3. *
  4. * @return \Illuminate\Broadcasting\PrivateChannel
  5. */
  6. public function broadcastOn()
  7. {
  8. return new PrivateChannel('order.'.$this->update->order_id);
  9. }

授权频道

请记住,只有授权过的用户才可以收听私有频道。我们可以在routes/channels.php 文件中,定义我们频道的授权规则。 在这个例子中,我们需要去验证任何试图收听 order.1 私有频道的用户,是否是订单实际上的创建者:

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

channel 方法接收两个参数: 频道的名称和一个通过返回 truefalse ,来表示用户是否有权收听该频道的回调函数 。

所有授权回调都将当前经过身份验证的用户作为其第一个参数,并将任何其他通配符参数作为其后续参数。 在这个例子中,我们用 {orderId} 占位符来通配表示频道名称 "ID" 的部分。

监听事件广播

接下来, 剩余的工作就是在我们的 JavaScript应用程序中监听事件。 我们可以使用 Laravel Echo 来做到这一点。首先,我们将使用private方法订阅私有频道。 然后,我们可以使用listen方法来监听 ShippingStatusUpdated 事件。 默认情况下,所有事件的公共属性都将包含在广播事件中:

  1. Echo.private(`order.${orderId}`)
  2. .listen('ShippingStatusUpdated', (e) => {
  3. console.log(e.update);
  4. });

定义广播事件

要通知 Laravel 应该广播给定事件, 并在该事件上实现 Illuminate\Contracts\Broadcasting\ShouldBroadcast 接口。此接口已导入到框架生成的所有事件类中,因此你可以轻松地将其添加到任何事件中。

ShouldBroadcast 接口要求你实现一个方法: broadcastOnbroadcastOn 方法应该返回事件应该广播的频道或频道数组。 该频道应该是 ChannelPrivateChannel,或者 PresenceChannel 的实例. 。Channel 的实例 代表任何用户可以订阅的公共频道, PrivateChannelsPresenceChannels 实列代表需要频道授权的私有频道:

  1. <?php
  2. namespace App\Events;
  3. use App\User;
  4. use Illuminate\Broadcasting\Channel;
  5. use Illuminate\Queue\SerializesModels;
  6. use Illuminate\Broadcasting\PrivateChannel;
  7. use Illuminate\Broadcasting\PresenceChannel;
  8. use Illuminate\Broadcasting\InteractsWithSockets;
  9. use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
  10. class ServerCreated implements ShouldBroadcast
  11. {
  12. use SerializesModels;
  13. public $user;
  14. /**
  15. * 新建一个新的事件实例
  16. *
  17. * @return void
  18. */
  19. public function __construct(User $user)
  20. {
  21. $this->user = $user;
  22. }
  23. /**
  24. * 获取事件应广播的频道。
  25. *
  26. * @return Channel|array
  27. */
  28. public function broadcastOn()
  29. {
  30. return new PrivateChannel('user.'.$this->user->id);
  31. }
  32. }

那么, 你仅仅需要像平常那样 触发事件 。事件一旦触发, 一个 队列任务 将通过你指定的广播驱动程序广播该事件。

广播名称

通常,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 方法接受两个参数:频道的名称和返回 truefalse 来指示用户是否有权在频道上进行侦听的回调。

所有授权回调都将当前经过身份验证的用户作为其第一个参数,并将任何其他通配符参数作为其后续参数。 在这个例子中,我们用 {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 。 如果你使用 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-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 进行连接(默认情况下,当forceTLSfalse时,如果页面是通过 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 频道,则应返回falsenull

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 方法一起展示,允许你订阅 herejoinleaving事件。

Echo.join(`chat.${roomId}`)
    .here((users) => {
        //
    })
    .joining((user) => {
        console.log(user.name);
    })
    .leaving((user) => {
        console.log(user.name);
    });

一旦成功加入频道,将立即执行here回调,并且将接收包含当前订阅该频道的所有其他用户的用户信息的数组。 当新用户加入频道时,将执行join方法,而当用户离开频道时,将执行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) => {
        //
    });

客户端事件

{提示} 使用 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);
    });

在此示例中,回调将接收通过 广播 频道发送到 App\User 实例的所有通知。 App.User.{id} 频道的频道授权回调包含在 Laravel 框架附带的默认BroadcastServiceProvider中。

本文章首发在 LearnKu.com 网站上。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接 我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。