Fork me on GitHub

laravel5.5事件广播系统

1. 定义广播事件

要告知 Laravel 一个给定的事件是广播类型,只需在事件类中实现 Illuminate\Contracts\Broadcasting\ShouldBroadcast 接口即可。

ShouldBroadcast 接口要求你实现一个方法:broadcastOn. broadcastOn 方法返回一个频道或一个频道数组,事件会被广播到这些频道。

频道必须是 Channel、PrivateChannel 或 PresenceChannel 的实例。Channel 实例表示任何用户都可以订阅的公开频道,而 PrivateChannels 和 PresenceChannels 则表示需要 频道授权 的私有频道:

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

1.1 广播名称

Laravel 默认会使用事件的类名作为广播名称来广播事件。不过,你也可以在事件类中通过定义一个 broadcastAs 方法来自定义广播名称:

public function broadcastAs()
{
    return 'server.created';
}

如果您使用 broadcastAs 方法自定义广播名称,你需要在你使用订阅事件的时候为事件类加上 . 前缀。这将指示 Echo 不要将应用程序的命名空间添加到事件中:

.listen('.server.created', function (e) {
    ....
});

1.2 广播数据

当一个事件被广播时,它所有的 public 属性会自动被序列化为广播数据,举个例子,如果你的事件有一个公有的 $user 属性,它包含了一个 Elouqent 模型,那么事件的广播数据会是:

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

然而,如果你想更细粒度地控制你的广播数据,你可以添加一个 broadcastWith 方法到你的事件中。这个方法应该返回一个数组,该数组中的数据会被添加到广播数据中,如果使用此方法,那么public属性不会被自动序列化为广播数据。


public function broadcastWith()
{
    return ['id' => $this->user->id];
}

1.3 广播队列

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

public $broadcastQueue = 'your-queue-name';

1.4 广播条件

有时,你想在给定条件为 true ,才广播你的事件。你可以通过在事件类中添加一个 broadcastWhen 方法来定义这些条件:

public function broadcastWhen()
{
    return $this->value > 100;
}

2. 频道授权

打开服务提供器 app/Providers/BroadcastServiceProvider.php

public function boot()
{   
    Broadcast::routes();

    require base_path('routes/channels.php');
}

发现boot()方法做了两件事,定义授权路由 和 定义授权回调

2.1 定义授权路由

对于私有频道,用户只有被授权后才能监听,这如何进行判定呢?

在使用 Laravel Echo 时,laravel-echo会自动向你的 Laravel 应用程序发起一个携带频道名称的 HTTP 请求,你的应用程序判断该用户是否能够监听该频道,所以要定义一个检测授权是否合法的路由。

这个路由就是通过boot()方法中的 Broadcast::routes 注册的,Broadcast::routes 方法会自动把它的路由放进 web 中间件组中

php artisan route:list
+--------+----------------------------------------+--------------------------------+-----------------------+-----------------------------------------------------------+-----------------+
| Domain | Method                                 | URI                            | Name                  | Action                                                    | Middleware      |
+--------+----------------------------------------+--------------------------------+-----------------------+-----------------------------------------------------------+-----------------+

|        | POST                                   | broadcasting/auth              |                       | \Illuminate\Broadcasting\BroadcastController@authenticate | web             |

2.2 定义授权回调

routes/channels.php

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

在这里我们需要定义真正用于处理频道授权的逻辑,channel 方法接收两个参数:频道名称和一个回调函数,该回调通过返回 true 或 false 来表示用户是否被授权监听该频道。

所有的授权回调接收当前被认证的用户作为第一个参数,任何额外的通配符参数作为后续参数。

  • 可以利用显示或者隐式 路由模型 绑定
use App\Order;

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

3. 对事件进行广播

3.1 可以使用event()方法触发

event(new ShippingStatusUpdated($update));

3.2 也可以使用broadcast()辅助函数

broadcast(new ShippingStatusUpdated($update));

//不同的是 broadcast 函数有一个 toOthers 方法允许你将当前用户从广播接收者中排除:
broadcast(new ShippingStatusUpdated($update))->toOthers();

3.3 只广播给他人

当你实例化 Laravel Echo 实例时,一个套接字 ID 会被指定到该连接。如果你使用 Vue 和 Axios,套接字 ID 会自动被添加到每一个请求的 X-Socket-ID 头中。然后,当你调用 toOthers 方法时,Laravel 会从头中提取出套接字 ID,并告诉广播器不要广播任何消息到该套接字 ID 的连接上。

如果你没有使用 Vue 和 Axios,则需要手动配置 JavaScript 应用程序来发送 X-Socket-ID 头。你可以用 Echo.socketId 方法来获取套接字 ID:

var socketId = Echo.socketId();

关于如何增加头部信息,分析laravel-echo的源码之后,发现修改Echo.connector.options的头信息应该可以完成功能,但是遇到一个坑,就是Echo.socketId()的获取会有延迟,一开始就直接拿会undefined,以下是我测试的解决方案

setTimeout(function() { 
    Echo.connector.options.auth.headers['X-Socket-ID'] = Echo.socketId();
}, 2000);

setTimeout(function() { 
    var orderId = 1
    Echo.private('order.' + orderId)
        .listen('TestPrivateEvent', (e) => {
            console.log(e);
        });
}, 3000);

上面的路由信息知道 验证方法在 \Illuminate\Broadcasting\BroadcastController@authenticate,我们找到文件打下日志信息,查看头部信息是否有socketId

public function authenticate(Request $request)
{   
    info(json_encode($request->header()));
    return Broadcast::auth($request);
}

日志信息如下

{
    "x-requested-with": [
        "XMLHttpRequest"
    ], 
    "cookie": [
        ......
    ], 
    "x-socket-id": [
        "2lZruXuFAFDeK6tKAABB"
    ], 
    "x-csrf-token": [
        "eci68phBwGXOjHJVNXslx6l39S9WXshO2KGdMN2a"
    ]
    
    ...
}

后端可以拿到x-socket-id信息,但是感觉不是太好,有更好的方法大家可以交流。

4. 接受广播

4.1 安装 Laravel Echo

Laravel Echo 是一个 JavaScript 库,它使得订阅频道和监听由 Laravel 广播的事件变得非常容易。你可以通过 NPM 包管理器来安装 Echo。仅讨论使用redis的情况

npm install laravel-echo --save  # 安装laravel-echo 并记录package.json

4.2 创建一个全新的 Echo 实例

官方说法是在resources/assets/js/bootstrap.js文件底部引入是个好地方

import Echo from "laravel-echo"

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

但是如果使用传统的blade模板,没有使用vue等前端,打包后发现#app未定义,并且会打包进去vue等我们不需要的内容,文件也会变大,

所以我修改resource/assets/js/app.js,直接打包我们需要的内容

import Echo from "laravel-echo"

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

4.3 使用laravel-mix打包

修改 webpack.mix.js

let mix = require('laravel-mix');

mix.js('resources/assets/js/app.js', 'public/js');
//   .sass('resources/assets/sass/app.scss', 'public/css');

生成

npm run dev

这样我们就得到了一个压缩的public/app.js文件

4.4 使用Echo实例监听

4.4.1 基本用法

Laravel Echo 会需要访问当前会话的 CSRF 令牌,可以在应用程序的 head HTML 元素中定义一个 meta 标签:

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

引入js文件

// 引入Socket.IO JavaScript 客户端库
<script src="//{{ Request::getHost() }}:6001/socket.io/socket.io.js"></script>

//实例化Echo
<script src="/js/app.js"></script>
<script>
    // 上面app.js已经进行了Echo的实例化,然后应该使用实例化的Echo进行广播事件的监听
    Echo.channel('orders')
    .listen('OrderShipped', (e) => {
        console.log(e.order.name);
    });
</script>

4.4.2 监听一个私有频道

方法

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

官方文档可能说的不是太清楚,实际${orderId}是个占位符,你在实际使用的时候可能需要根据实际情况获取到这个值,比如

var order_id = { $order_id }

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

4.4.3 链式调用监听一个频道上多个事件

Echo.private('orders')
    .listen(...)
    .listen(...)
    .listen(...);

4.5 退出频道

Echo.leave('orders');

4.6 命名空间

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

5 Presence 频道

Presence 频道是在私有频道的安全性基础上,额外暴露出有哪些人订阅了该频道。这使得它可以很容易地建立强大的、协同的应用,如当有一个用户在浏览页面时,通知其他正在浏览相同页面的用户。

5.1 授权 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];
    }
});

5.2 加入 Presence 频道

你可以用 Echo 的 join 方法来加入 presence 频道。join 方法会返回一个实现了 PresenceChannel 的对象,它通过暴露 listen 方法,允许你订阅 here、joining 和 leaving 事件。

Echo.join(`chat.${roomId}`)
    .here((users) => {
        //
    })
    .joining((user) => {
        console.log(user.name);
    })
    .leaving((user) => {
        console.log(user.name);
    });
  • here 回调函数会在你成功加入频道后被立即执行,它接收一个包含用户信息的数组,用来告知当前订阅在该频道上的其他用户。
  • joining 方法会在其他新用户加入到频道时被执行,
  • leaving 会在其他用户退出频道时被执行。

5.3 广播到 Presence 频道

Presence 频道可以像公开和私有频道一样接收事件。使用一个聊天室的例子,我们要把 NewMessage 事件广播到聊天室的 presence 频道。要实现它,我们将从事件的 broadcastOn 方法中返回一个 PresenceChannel 实例:

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

6. 客户端事件

有时候你可能希望广播一个事件给其他已经连接的客户端,但并不需要通知你的 Laravel 程序。试想一下当你想提醒用户,另外一个用户正在输入中的时候,显而易见客服端事件对于「输入中」之类的通知就显得非常有用了。你可以使用 Echo 的 whisper 方法来广播客户端事件:

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

你可以使用 listenForWhisper 方法来监听客户端事件:

Echo.channel('chat')
    .listenForWhisper('typing', (e) => {
        console.log(e.name);
    });
posted @ 2018-03-27 17:37  archer-wong  阅读(529)  评论(0编辑  收藏  举报