[ Laravel 5.3 文档 ] 综合话题 —— 事件广播

1、简介

在很多现代 Web 应用中,Web 套接字(WebSockets)被用于实现实时更新的用户接口。当一些数据在服务器上被更新,通常一条消息通过 websocket 连接被发送给客户端处理。这为我们提供了一个更强大的、更有效的选择来持续拉取应用的更新。

为帮助你构建这样的应用,Laravel 让通过 websocket 连接广播事件变得简单。广播 Laravel 事件允许你在服务端和客户端 JavaScript 框架之间共享同一事件名。

注:在深入了解事件广播之前,确保你已经阅读并理解Laravel事件与监听器相关文档

配置

应用的所有事件广播配置选项都存放在 config/broadcasting.php 配置文件中。Laravel 开箱支持多种广播驱动:PusherRedis以及一个服务于本地开发和调试的日志驱动。此外,还提供了一个null驱动用于完全禁止事件广播。每一个驱动在config/broadcasting.php配置文件中都有一个配置示例。

广播服务提供者

在广播任意事件之前,首先需要注册App\Providers\BroadcastServiceProvider。在新安装的Laravel应用中,你只需要取消config/app.php配置文件中providers数组内对应服务提供者之前的注释即可。该提供者允许你注册广播认证路由和回调。

CSRF令牌

Laravel Echo需要访问当前session的CSRF令牌(token),如果有效的话,Echo将会从JavaScript变量Laravel.csrfToken中获取令牌。如果你运行过Artisan命令make:auth的话,该对象定义在resources/views/layouts/app.blade.php布局文件中。如果你没有使用这个布局,你可以在应用的HTML元素head中定义一个meta标签:

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

驱动预备知识

Pusher

如果你准备通过Pusher广播事件,需要使用Composer包管理器安装对应的Pusher PHP SDK:

composer require pusher/pusher-php-server

接下来,你需要在config/broadcasting.php配置文件中配置你的Pusher证书。一个配置好的Pusher示例已经包含在这个文件中,你可以按照这个模板进行修改,指定自己的Pusher key、secret和应用ID即可。

使用Pusher和Laravel Echo的时候,需要在安装某个Echo实例的时候指定pusher作为期望的广播:

import Echo from "laravel-echo"

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: 'your-pusher-key'
});

Redis

如果你使用Redis广播,需要安装Predis库:

composer require predis/predis

Redis广播使用Redis的pub/sub功能进行广播;不过,你需要将其和能够接受Redis消息的Websocket服务器进行配对以便将消息广播到Websocket频道

当Redis广播发布事件时,事件将会被发布到指定的频道上,传递的数据是一个JSON格式的字符串,其中包含了事件名称、数据明细data、以及生成事件socket ID的用户。

Socket.IO

如果你想配对Redis广播和Socket.IO服务器,则需要在应用的HTML元素head中引入Socket.IO JavaScript库:

<script src="https://cdn.socket.io/socket.io-1.4.5.js"></script>

接下来,你需要使用socket.io连接器和host来实例化Echo。例如,如果你的应用和socket服务器运行在了app.dev上,那么需要注意实例化Echo:

import Echo from "laravel-echo"

window.Echo = new Echo({
    broadcaster: 'socket.io',
    host: 'http://app.dev:6001'
});

最后,需要运行一个与之兼容的Socket.IO服务器。Laravel并未内置一个Socket.IO服务器实现,不过,这里有一个第三方实现的Socket.IO驱动:tlaverdure/laravel-echo-server

队列预备知识

在开始介绍广播事件之前,还需要配置并运行一个队列监听器。所有事件广播都通过队列任务来完成以便应用的响应时间不受影响。

2、概念概览

Laravel的事件广播允许你使用基于驱动的WebSockets将服务器端端事件广播到客户端JavaScript应用。目前,Laravel使用Pusher和Redis驱动,这些事件可以通过JavaScript包Laravel Echo在客户端被轻松消费。

事件通过“频道”进行广播,这些频道可以是公共的,也可以是私有的,应用的任何访问者都可以不经认证和授权注册到一个共有的频道,不过,想要注册到私有频道,用户必须经过认证和授权才能监听该频道。

使用示例应用

在深入了解每个事件广播组件之前,让我们先通过一个电商网站作为示例对整体有个大致的了解。这里我们不会讨论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
{
    //
}


ShouldBroadcast
接口要求事件类定义一个broadcastOn方法,该方法会返回事件将要广播的频道。事件类生成时一个空的方法存根已经定义,我们所要做的只是填充其细节。我们只想要订单的创建者能够察看状态更新,所以我们将这个事件广播在一个与订单绑定的私有频道上:

/**
 * Get the channels the event should broadcast on.
 *
 * @return array
 */
public function broadcastOn()
{
    return new PrivateChannel('order.'.$this->update->order_id);
}

授权频道

记住,用户必须经过授权之后才能监听私有频道。我们可以在BroadcastServiceProviderboot方法中定义频道授权规则。在本例中,我们需要验证任意试图监听order.1频道的用户确实是订单的创建者:

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

channel方法接收两个参数:频道的名称以及返回truefalse以表明用户是否被授权可以监听频道的回调。

所有授权回调都接收当前认证用户作为第一个参数以及任意额外通配符参数作为随后参数,在本例中,我们使用*字符标识频道名称的ID部分是一个通配符。

监听事件广播

接下来要做的就是在JavaScript中监听事件。我们可以使用Laravel Echo来完成这一工作。首先,我们使用private方法注册私有频道。然后,我们使用listen方法监听ShippingStatusUpdated事件。默认情况下,所有事件的公共属性都要包含在广播事件中:

Echo.private('order.' + orderId)
    .listen('ShippingStatusUpdated', (e) => {
        console.log(e.update);
    });

3、定义广播事件

要告诉 Laravel 给定事件应该被广播,需要在事件类上实现Illuminate\Contracts\Broadcasting\ShouldBroadcast 接口,这个接口已经在Laravel框架生成的事件类中导入了,你只需要将其添加到事件即可。

ShouldBroadcast 接口要求你实现一个方法:broadcastOn。该方法应该返回一个事件广播频道或频道数组。这些频道必须是ChannelPrivateChannelPresenceChannel的实例,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;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(User $user)
    {
        $this->user = $user;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return Channel|array
     */
    public function broadcastOn()
    {
        return new PrivateChannel('user.'.$this->user->id);
    }
}

然后,你只需要正常触发这个事件即可。一旦事件被触发,队列任务会自动通过指定广播驱动广播该事件。

广播数据

如果某个事件被广播,其所有的 public 属性都会按照事件负载(payload)自动序列化和广播,从而允许你从 JavaScript 中访问所有public 数据,因此,举个例子,如果你的事件有一个单独的包含 Eloquent 模型的 $user 属性,广播负载定义如下:

{
    "user": {
    "id": 1,
        "name": "Patrick Stewart"
        ...
    }
}

不过,如果你希望对广播负载有更加细粒度的控制,可以添加 broadcastWith 方法到事件,该方法会返回你想要通过事件广播的数组数据:

/**
 * 获取广播数据
 *
 * @return array
 */
public function broadcastWith(){
    return ['user' => $this->user->id];
}

广播队列

默认情况下,每个广播事件都放置在配置文件 queue.php 中指定的默认队列连接中,你可以通过在事件类上定义一个 broadcastQueue 属性来自定义广播使用的队列。该属性需要指定广播时你想要使用的队列名称:

/**
 * The name of the queue on which to place the event.
 *
 * @var string
 */
public $broadcastQueue = 'your-queue-name';

4、授权频道

私有频道要求你授权当前认证用户可以监听的频道。这可以通过向Laravel发送包含频道名称的HTTP请求然后让应用判断该用户是否可以监听频道来实现。使用Laravel Echo的时候,授权注册到私有频道的HTTP请求会自动发送,不过,你也需要定义相应路由来响应这些请求。

定义授权路由

庆幸的是,Laravel让定义响应频道授权请求的路由变得简单,在Laravel自带的 BroadcastServiceProvider 中,你可以看到 Broadcast::routes 方法的调用,该方法会注册 /broadcasting/auth 路由来处理授权请求:

Broadcast::routes();

Broadcast::routes 方法将会自动将路由放置到web中间件组,不过,你也可以传递一个路由属性数组到这个方法以便自定义分配的属性:

Broadcast::routes($attributes);

定义授权回调

接下来,我们需要定义执行频道授权的逻辑,和定义授权路由一样,这也是通过 BroadcastServiceProvider 中的 boot 方法来完成。在这个方法中,你可以使用 Broadcast::channel 方法来注册频道授权回调:

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

channel 方法接收两个参数:频道名称和返回truefalse以标识用户是否授权可以监听该频道的回调。

所有授权回调都接收当前认证用户作为第一个参数以及任意额外通配符参数作为其他参数。在本例中,我们使用*字符来标识频道名称的ID部分是一个通配符。

5、广播事件

定义好事件并标记其实现 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应用从服务端接收到响应后,会直接将这个新任务插入到任务列表:

this.$http.post('/task', task)
    .then((response) => {
        this.tasks.push(response.data);
    });

不过,还记得吗?我们还广播了任务创建,如果你的JavaScript应用正在监听这个事件以便添加任务到任务列表,就会在列表中出现重复任务:一个来自服务端,一个来自广播。

你可以通过使用 toOthers 方法来解决这个问题,该方法告知广播不要讲事件广播给当前用户。

配置

当你初始化Laravel Echo实例的时候,需要给连接分配一个socket ID。如果你使用的是Vue和Vue资源,这个socket ID会以 X-Socket-ID 头的方式自动添加到每个输出请求,Laravel会从请求头中解析这个socket ID并告知广播不要广播到带有这个socket ID的连接。

如果你没有使用Vue和Vue资源,则需要手动配置JavaScript应用发送 X-Socket-ID 请求头。你可以使用 Echo.socketId 方法获取这个socket ID:

var socketId = Echo.socketId();

6、接收广播

安装Laravel Echo

Laravel Echo 是一个JavaScript库,有了它之后,注册到频道监听Laravel广播的事件将变得轻而易举。你可以通过NPM包管理器安装Echo,在本例中,我们还会安装 pusher-js 包,因为我们将会使用Pusher进行广播:

npm install --save laravel-echo pusher-js

创建好Echo之后,就可以在应用的JavaScript中创建一个新的Echo实例,做这件事的最佳位置当然是在Laravel自带的resources/assets/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会默认事件都位于 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) => {
        //
    });

7、存在频道(Presence Channel)

存在频道构建于私有频道之上,并且暴露了额外功能:获知谁注册到频道。基于这一点,我们可以构建强大的、协作的应用功能,例如当其他用户访问同一个页面时通知当前用户。

授权存在频道

所有存在频道同时也是私有频道,因此,用户必须被授权访问权限。不过,当定义存在频道的授权回调时,如果用户被授权加入频道不要返回true,取而代之地,你应该返回关于该用户数据的数组。

授权回调返回的数据在JavaScript应用的存在频道事件监听器中使用,如果用户没有被授权加入存在频道,应该返回falsenull

Broadcast::channel('chat.*', function ($user, $roomId) {
    if ($user->canJoinRoom($roomId)) {
        return ['id' => $user->id, 'name' => $user->name];
    }
});

加入授权频道

要加入存在频道,可以使用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 方法则在用户离开频道时执行。

广播到存在频道

存在频道可以像公共或私有频道一样接收事件,以聊天室为例,我们可能想要广播 NewMessage 事件到房间的存在频道,要实现这个,需要从事件的 broadcastOn 方法返回 PresenceChannel 实例:

/**
 * Get the channels the event should broadcast on.
 *
 * @return Channel|array
 */
public function broadcastOn()
{
    return new PresenceChannel('room.'.$this->message->room_id);
}

和公共或私有频道一样,存在频道事件可以使用 broadcast 函数进行广播。和其他事件一样,你可以使用 toOthers 方法从所有接收广播的用户中排除当前用户:

broadcast(new NewMessage($message));
broadcast(new NewMessage($message))->toOthers();

你可以通过Echo的 listen 方法监听加入事件:

Echo.join('chat.' + roomId)
    .here(...)
    .joining(...)
    .leaving(...)
    .listen('NewMessage', (e) => {
        //
    });

8、通知

通过配对事件广播和通知,JavaScript应用可以在事件发生时无需刷新当前页面接收新的通知。首先,确保你已经通读广播通知频道文档。

配置好使用广播频道的通知后,可以通过使用Echo的 notification 方法监听广播事件,记住,频道名称应该和接收通知的类名保持一致:

Echo.private('App.User.' + userId)
    .notification((notification) => {
        console.log(notification.type);
    });

在这个例子中,所有通过 broadcast 频道发送给 App\User 实例的通知都会被这个回调接收。Laravel框架内置的 BroadcastServiceProvider 中包含了一个针对 App.User.* 频道的频道授权回调。

学院君

学院君 has written 550 articles

资深PHP工程师,Laravel学院院长