Laravel 中服务端与客户端事件广播实现


简介

在很多现代 Web 应用中,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需要访问当前 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 "~3.0"

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

'options' => [
    'cluster' => 'eu',
    'encrypted' => true
], 

使用 Pusher 和 Laravel Echo 的时候,需要在 resources/assets/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 的发布/订阅功能进行广播;不过,你需要将其和能够接受 Redis 消息的 Websocket 服务器进行配对以便将消息广播到 Websocket 频道。

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

Socket.IO

如果你想配对 Redis 广播和 Socket.IO 服务器,则需要在应用的 HTML 元素 head 中引入 Socket.IO JavaScript 库。当 Socket.IO 服务器启动后,会自动通过一个标准的 URL 来暴露客户端 JavaScript 库,例如,如果你在同一个域名下运行 Socket.IO 和 Web 应用,可以这样访问客户端 JavaScript 库:

<script src="//{{ Request::getHost() }}:6001/socket.io/socket.io.js"></script>

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

import Echo from "laravel-echo"

window.Echo = new Echo({
    broadcaster: 'socket.io',
    host: window.location.hostname + ':6001'
});

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

队列预备知识

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

概念概览

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

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

示例应用

在深入了解每个事件广播组件之前,让我们先通过一个电商网站作为示例对整体有个大致的了解。这里我们不会讨论 PusherLaravel 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
{
    /**
     * 物流状态更新信息.
     *
     * @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 接口,这个接口已经在 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;

    /**
     * 创建一个新的事件实例.
     *
     * @return void
     * @translator laravelacademy.org
     */
    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 属性都会按照事件负载(payload)自动序列化和广播,从而允许你从 JavaScript 中访问所有 public 数据,举个例子,如果你的事件有一个单独的包含 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);

定义授权回调

接下来,我们需要定义执行频道授权的逻辑,这可以通过应用自带的 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;
});

广播事件

定义好事件并标记其实现 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-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 实例上调用 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 Channel)

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

授权存在频道

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

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

Broadcast::channel('chat.{roomId}', 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 实例:

/**
 * 获取事件广播频道.
 *
 * @return Channel|array
 * @translator laravelacademy.org
 */
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) => {
        //
    });

客户端事件

有时候你可能想要广播事件到其他连接到的客户端,而不经过 Laravel 应用,这在处理“输入”通知这种事情时尤其有用,比如告知应用的用户其他用户正在给定屏幕输入信息。要广播客户端事件,可以使用 Echo 的 whisper 方法:

Echo.channel('chat')
    .whisper('typing', {
        name: this.user.name
    });

要监听客户端事件,可以使用 listenForWhisper 方法:

Echo.channel('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 实例的通知都会被这个回调接收。Laravel 框架内置的 BroadcastServiceProvider 中包含了一个针对 App.User.{id} 频道的频道授权回调。


点赞 取消点赞 收藏 取消收藏

<< 上一篇: Laravel 队列系统实现及使用教程

>> 下一篇: 通过缓存构建高性能 Laravel 应用