[toc]
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\[email protected] | 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\[email protected],我们找到文件打下日志信息,查看头部信息是否有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);
});