广播
简介
在很多现代 Web 应用中,WebSocket 被用来实现实时、动态更新的用户界面。当服务端某些数据更新时,通常会通过 WebSocket 连接发送一条消息给客户端处理。WebSocket 提供了一种比不断轮询应用服务器获取数据变更更高效的方式。
例如,假设你的应用能把用户数据导出为 CSV 文件并发邮件给他。不过,创建这个 CSV 需要几分钟,所以你选择在队列任务中创建并发送 CSV。当 CSV 创建完成并邮寄给用户后,我们可以用事件广播来分发一个 App\Events\UserDataExported 事件,让前端 Vue 应用接收。收到事件后,就可以在不刷新页面的情况下向用户显示 CSV 已发送的消息。
为了帮你构建这类功能,Laravel 让你可以轻松地通过 WebSocket 连接「广播」服务端的 Laravel 事件。广播你的 Laravel 事件可以让你在服务端 Laravel 应用和客户端 JavaScript 应用之间共享相同的事件名称和数据。
广播的核心概念很简单:客户端在前端连接到命名的通道,而 Laravel 应用在后端向这些通道广播事件。这些事件可以包含你想提供给前端的任何额外数据。
支持的驱动
Laravel 内置了 Laravel Reverb、Pusher Channels 和 Ably 三个服务端广播驱动。其中 Reverb 是 Laravel 官方出品的第一方 WebSocket 服务器,用 PHP 编写,专门设计为自托管——消息完全在你自己的服务器上流转,不依赖任何第三方服务。Pusher Channels 和 Ably 则是 SaaS 服务,消息需要经过它们的服务器。
本文只讲 Reverb,适合不想依赖外部服务的项目。Reverb 使用 Pusher 协议进行 WebSocket 通信,所以前端需要安装 pusher-js 包来建立 WebSocket 连接——但这只是协议兼容,跟 Pusher 的 SaaS 服务没有任何关系。
信息
在深入了解事件广播之前,请确保你已经阅读了 Laravel 的事件和监听器文档。
快速入门
整个广播系统的搭建分为服务端(Laravel API)和客户端(Vue 前端)两部分。下面是完整的步骤概览:
服务端(Laravel API):
- 安装 Reverb 包并初始化
- 配置
.env环境变量 - 配置
bootstrap/app.php让广播授权走 API + Sanctum 中间件 - 在
routes/channels.php中定义通道授权规则
客户端(Vue 前端):
- 安装
laravel-echo、pusher-js、@laravel/echo-vue - 配置 Echo 实例(含 Sanctum token 授权)
- 在 Vue 组件中用
useEcho监听事件
运行:
- 启动 Reverb WebSocket 服务器、队列 worker、Laravel API、Vue 开发服务器
- 触发事件,前端实时接收
接下来一步步展开。
下一步
看完安装部分后,概念概述里有一个从后端到前端的完整案例,把事件定义、通道授权、API 触发、Vue 监听串在一起,可以直接跑通。
注意
在广播任何事件之前,你应该先配置并运行一个队列 worker。所有事件广播默认通过队列任务完成,这样广播事件就不会严重影响应用的响应时间。如果你想在开发时跳过队列立即广播,可以实现 ShouldBroadcastNow 接口替代 ShouldBroadcast。
服务端安装
这一步在 Laravel API 项目中安装 Reverb,让它充当 WebSocket 服务器。
Reverb
使用 Reverb 作为事件广播器时,带 --reverb 选项运行 install:broadcasting Artisan 命令即可。这个命令会安装 Reverb 的 Composer 包,创建 config/broadcasting.php 配置文件和 routes/channels.php 文件,并更新 .env 中的相应变量:
php artisan install:broadcasting --reverb
提示
install:broadcasting 命令还会尝试安装前端 NPM 包并修改 resources/js/app.js。如果你的 Laravel 项目是纯 API(没有 resources/js/ 目录),这些前端步骤可以忽略——前端 Echo 的安装和配置在独立的 Vue 项目中完成,见客户端安装。
手动安装
如果你想完全手动控制安装过程,按以下步骤操作。
第一步:安装 Reverb Composer 包
composer require laravel/reverb
第二步:运行 Reverb 安装命令
php artisan reverb:install
这个命令会发布 config/reverb.php 配置文件,生成 Reverb 所需的 APP_ID、APP_KEY、APP_SECRET,并在 .env 中添加 Reverb 环境变量。
第三步:配置 .env
确保 .env 中有以下配置:
# 广播驱动改为 reverb
BROADCAST_CONNECTION=reverb
# 队列驱动(广播事件默认走队列,必须配置)
QUEUE_CONNECTION=database
# Reverb 配置(reverb:install 会自动生成这些值)
REVERB_APP_ID=your-app-id
REVERB_APP_KEY=your-app-key
REVERB_APP_SECRET=your-app-secret
REVERB_HOST="localhost"
REVERB_PORT=8080
REVERB_SCHEME=http
提示
生产环境应使用 HTTPS(REVERB_SCHEME=https,REVERB_PORT=443),并通过 Nginx 反向代理将 WebSocket 流量转发到 Reverb 服务器。
第四步:确保 routes/channels.php 存在
如果 reverb:install 没有自动创建 routes/channels.php,手动创建一个空文件:
<?php
use Illuminate\Support\Facades\Broadcast;
// 在这里注册通道授权规则
第五步:配置广播授权走 API + Sanctum
由于本项目是纯 API 架构(前端通过 Sanctum token 认证,不走 session),需要让广播授权端点也走 API 中间件。在 bootstrap/app.php 中添加 withBroadcasting():
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
commands: __DIR__.'/../routes/console.php',
using: function () {
Route::middleware('api')
->prefix('api')
->group(base_path('routes/front/api.php'));
Route::middleware('api')
->prefix('api/admin')
->group(base_path('routes/admin/api.php'));
},
)
->withBroadcasting(
__DIR__.'/../routes/channels.php',
['prefix' => 'api', 'middleware' => ['api', 'auth:sanctum']],
)
// ...其余配置
这样,广播授权端点就变成了 POST /api/broadcasting/auth,使用 auth:sanctum 中间件认证用户。前端 Echo 订阅私有通道时,会自动向这个端点发送授权请求。
你可以在 Reverb 文档中找到更多安装和使用说明。
客户端安装
这一步在独立的 Vue 前端项目中安装 Laravel Echo,让它能连接 Reverb 服务器并接收广播事件。
Reverb
Laravel Echo 是一个 JavaScript 库,让你轻松订阅通道并监听服务端广播的事件。@laravel/echo-vue 是 Echo 的 Vue 适配包,提供 useEcho 等组合式 API,在组件卸载时自动清理监听。
手动安装
第一步:在 Vue 项目中安装依赖
npm install laravel-echo pusher-js @laravel/echo-vue
信息
pusher-js 是必须的——Reverb 使用 Pusher 协议进行 WebSocket 通信,所以前端用 pusher-js 建立连接。这跟 Pusher 的 SaaS 服务无关,只是协议兼容。
第二步:配置 Vue 项目的 .env
在 Vue 项目的 .env 文件中添加 Reverb 连接信息:
# Laravel API 地址
VITE_API_URL=http://localhost:8000
# Reverb WebSocket 连接信息(和服务端 .env 中的值对应)
VITE_REVERB_APP_KEY=your-app-key
VITE_REVERB_HOST=localhost
VITE_REVERB_PORT=8080
VITE_REVERB_SCHEME=http
第三步:创建 Echo 配置文件
在 Vue 项目中创建 src/lib/echo.ts:
import { configureEcho } from '@laravel/echo-vue';
import axios from 'axios';
configureEcho({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
// 私有通道授权:每次授权都从 localStorage 读取最新 token,
// 这样登录/登出后 token 自动更新。
authorizer: (channel) => ({
authorize: (socketId, callback) => {
axios.post(`${import.meta.env.VITE_API_URL}/broadcasting/auth`, {
socket_id: socketId,
channel_name: channel.name,
}, {
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
Accept: 'application/json',
},
}).then(response => callback(false, response.data))
.catch(error => callback(true, error));
},
}),
});
注意
authorizer 回调里用 localStorage.getItem('token') 动态读取 token,而不是在初始化时固定写死。这是因为用户可能登录/登出,token 会变化——如果写死,旧 token 会导致授权失败。把 localStorage.getItem('token') 换成你项目实际存储 token 的方式。
第四步:在应用入口导入 Echo 配置
在 src/main.ts 中导入 echo.ts,确保在 Vue 应用挂载之前执行 configureEcho():
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
import './lib/echo'; // 配置 Laravel Echo
import './assets/main.css';
const app = createApp(App);
app.use(createPinia());
app.use(router);
app.mount('#app');
第五步:启动 Reverb 服务器并验证
在 Laravel 项目中启动 Reverb WebSocket 服务器:
php artisan reverb:start
如果一切正常,你会看到 Reverb 服务器在 ws://localhost:8080 上运行的输出。同时,Vue 开发服务器启动后,打开浏览器控制台,如果 Echo 连接成功,不会出现 WebSocket 连接错误。
信息
完整的服务启动流程见概念概述中的完整案例,那里会展示如何同时运行 Reverb、队列 worker、Laravel API 和 Vue 前端,并端到端测试整个广播流程。
使用已有客户端实例
如果你已经有一个预配置的 pusher-js 客户端实例想让 Echo 复用,可以通过 client 配置选项传入:
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
const options = {
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
};
const echo = new Echo({
...options,
client: new Pusher(options.key, options),
});
这种方式适用于你需要对 pusher-js 底层客户端做额外自定义(比如自定义日志、集群配置等)的场景。
概念概述
Laravel 的事件广播允许你通过 WebSocket 将服务端 Laravel 事件广播到客户端 JavaScript 应用。我们使用 Reverb 作为 WebSocket 服务器,前端用 Laravel Echo 订阅通道并接收事件。
事件通过「通道」广播,通道分为公共和私有两种。任何人都可以订阅公共通道,无需认证;私有通道则要求用户经过认证并获得授权才能监听。
下面用一个完整的订单状态更新案例,把所有环节串起来——从后端事件定义到前端 Vue 组件接收,每一步都能跑通。
示例应用
假设我们的应用中有一个页面让用户查看订单的发货状态。当发货状态更新时,我们希望前端实时收到通知,而不是刷新页面。
ShouldBroadcast 接口
第一步:创建广播事件
先用 Artisan 命令生成事件类:
php artisan make:event OrderShipmentStatusUpdated
然后编辑 app/Events/OrderShipmentStatusUpdated.php,让它实现 ShouldBroadcast 接口:
<?php
namespace App\Events;
use App\Models\Order;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels;
class OrderShipmentStatusUpdated implements ShouldBroadcast
{
use InteractsWithSockets, SerializesModels;
public function __construct(
public Order $order,
) {}
/**
* 事件应该广播到哪些通道。
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('orders.'.$this->order->id),
];
}
/**
* 广播载荷数据。
*
* @return array<string, mixed>
*/
public function broadcastWith(): array
{
return [
'id' => $this->order->id,
'status' => $this->order->status,
'updated_at' => $this->order->updated_at->toDateTimeString(),
];
}
}
ShouldBroadcast 接口要求实现 broadcastOn() 方法,返回事件应该广播到的通道。这里我们用 PrivateChannel,表示只有订单的创建者才能监听。broadcastWith() 方法自定义广播载荷——如果不定义,默认会序列化事件的所有 public 属性。
提示
InteractsWithSockets trait 是调用 toOthers() 方法的前提(见只发给其他人)。SerializesModels trait 确保 Eloquent 模型在队列任务中被正确序列化和反序列化。
授权通道
用户必须经过授权才能监听私有通道。在 routes/channels.php 中定义授权规则:
<?php
use App\Models\Order;
use App\Models\User;
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('orders.{orderId}', function (User $user, int $orderId) {
$order = Order::findOrNew($orderId);
return $user->id === $order->user_id;
});
channel() 方法接收两个参数:通道名和一个回调函数。回调返回 true 表示用户有权监听该通道,返回 false 表示无权。回调的第一个参数始终是当前认证用户(通过 Sanctum token 认证),后续参数是通道名中的通配符 {orderId} 的值。
第三步:创建 API 端点触发事件
在控制器中触发事件:
<?php
namespace App\Http\Controllers;
use App\Events\OrderShipmentStatusUpdated;
use App\Models\Order;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class OrderController extends Controller
{
public function updateStatus(Request $request, Order $order): JsonResponse
{
$order->update([
'status' => $request->input('status'),
]);
// 触发广播事件
OrderShipmentStatusUpdated::dispatch($order);
return response()->json([
'message' => '订单状态已更新',
'order' => $order,
]);
}
}
在 routes/front/api.php 中注册路由:
use App\Http\Controllers\OrderController;
Route::put('/orders/{order}/status', [OrderController::class, 'updateStatus'])
->middleware('auth:sanctum');
监听事件广播
第四步:在 Vue 组件中监听事件
确保已完成客户端安装,然后在 Vue 组件中用 useEcho 监听私有通道事件:
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useEcho } from '@laravel/echo-vue';
import axios from 'axios';
const orderId = ref(1);
const status = ref<string | null>(null);
const lastUpdated = ref<string | null>(null);
// 监听私有通道 orders.{orderId} 上的 OrderShipmentStatusUpdated 事件
// 组件卸载时自动离开通道,无需手动清理
useEcho(
`orders.${orderId.value}`,
'OrderShipmentStatusUpdated',
(e: { id: number; status: string; updated_at: string }) => {
status.value = e.status;
lastUpdated.value = e.updated_at;
console.log('收到广播事件:', e);
},
);
// 触发广播:调用 API 更新订单状态
async function updateStatus(newStatus: string) {
await axios.put(`/api/orders/${orderId.value}/status`, {
status: newStatus,
}, {
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
}
</script>
<template>
<div>
<p>订单 #{{ orderId }} 状态: {{ status ?? '加载中...' }}</p>
<p v-if="lastUpdated">最后更新: {{ lastUpdated }}</p>
<button @click="updateStatus('shipped')">标记为已发货</button>
</div>
</template>
第五步:启动所有服务并测试
打开 4 个终端,分别运行:
# 终端 1:启动 Reverb WebSocket 服务器
php artisan reverb:start
# 终端 2:启动队列 worker(广播事件通过队列处理)
php artisan queue:listen
# 终端 3:启动 Laravel API 服务器
php artisan serve
# 终端 4:启动 Vue 前端开发服务器
cd /path/to/web && npm run dev
现在,在 Vue 页面点击「标记为已发货」按钮,API 会更新订单状态并触发 OrderShipmentStatusUpdated 事件。事件通过队列推送到 Reverb,Reverb 通过 WebSocket 推送给前端,Vue 组件的 useEcho 回调被触发,页面实时更新状态——整个过程不需要刷新页面。
提示
如果你在开发时不想每次都启动队列 worker,可以把事件接口从 ShouldBroadcast 改成 ShouldBroadcastNow,事件会同步广播,跳过队列。但这不适合生产环境,因为同步广播会阻塞 HTTP 响应。
定义广播事件
要告知 Laravel 某个事件应该被广播,在事件类上实现 Illuminate\Contracts\Broadcasting\ShouldBroadcast 接口。这个接口已经导入到框架生成的所有事件类中。
ShouldBroadcast 接口要求实现 broadcastOn() 方法,返回事件应该广播到的通道。通道可以是 Channel(公共通道)、PrivateChannel(私有通道)或 PresenceChannel(在线通道)。Channel 代表任何人都可以订阅的公共通道,PrivateChannel 和 PresenceChannel 代表需要通道授权的私有通道:
<?php
namespace App\Events;
use App\Models\User;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels;
class ServerCreated implements ShouldBroadcast
{
use SerializesModels;
public function __construct(
public User $user,
) {}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('user.'.$this->user->id),
];
}
}
实现 ShouldBroadcast 接口后,只需要像平常一样触发事件。事件触发后,一个队列任务会自动使用你指定的广播驱动来广播该事件。
广播名称
默认情况下,Laravel 会使用事件的类名来广播事件。你可以通过在事件上定义 broadcastAs() 方法来自定义广播名称:
/**
* The event's broadcast name.
*/
public function broadcastAs(): string
{
return 'server.created';
}
如果你用 broadcastAs() 自定义了广播名称,前端监听时要在事件名前加 .,指示 Echo 不要在事件名前加应用的命名空间:
Echo.private('user.1')
.listen('.server.created', (e) => {
// ...
});
广播数据
事件广播时,所有 public 属性会自动序列化并作为事件的载荷广播,让你可以在 JavaScript 应用中访问任何公共数据。比如,如果你的事件有一个包含 Eloquent 模型的公共 $user 属性,事件的广播载荷会是:
{
"user": {
"id": 1,
"name": "Patrick Stewart"
}
}
如果你想对广播载荷有更细粒度的控制,可以在事件上添加 broadcastWith() 方法。这个方法应返回你想作为事件载荷广播的数据数组:
/**
* Get the data to broadcast.
*
* @return array<string, mixed>
*/
public function broadcastWith(): array
{
return ['id' => $this->user->id];
}
广播队列
默认情况下,每个广播事件都会被放到 queue.php 配置文件中指定的默认队列连接的默认队列上。你可以通过在事件类上使用 Connection 和 Queue 属性来自定义广播器使用的队列连接和名称:
use Illuminate\Queue\Attributes\Connection;
use Illuminate\Queue\Attributes\Queue;
#[Connection('redis')]
#[Queue('default')]
class ServerCreated implements ShouldBroadcast
{
// ...
}
或者,你可以通过在事件上定义 broadcastQueue() 方法来自定义队列名:
/**
* The name of the queue on which to place the broadcasting job.
*/
public function broadcastQueue(): string
{
return 'default';
}
如果你想用 sync 队列而不是默认队列驱动来广播事件(开发调试时很有用),可以实现 ShouldBroadcastNow 接口来替代 ShouldBroadcast:
<?php
namespace App\Events;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
class OrderShipmentStatusUpdated implements ShouldBroadcastNow
{
// ...
}
广播条件
有时你可能只想在给定条件为真时才广播事件。你可以通过在事件类上添加 broadcastWhen() 方法定义这些条件:
/**
* Determine if this event should broadcast.
*/
public function broadcastWhen(): bool
{
return $this->order->value > 100;
}
广播与数据库事务
当广播事件在数据库事务内分发时,它们可能在数据库事务提交之前就被队列处理了。发生这种情况时,你在数据库事务期间对模型或数据库记录做的任何更新可能还没有反映到数据库中。此外,事务内创建的任何模型或数据库记录可能还不存在于数据库中。如果你的事件依赖这些模型,广播事件的任务被处理时可能会出现意外错误。
如果你的队列连接的 after_commit 配置选项设为 false,你仍然可以通过在事件类上实现 ShouldDispatchAfterCommit 接口来指示特定的广播事件应该在所有打开的数据库事务提交后再分发:
<?php
namespace App\Events;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;
use Illuminate\Queue\SerializesModels;
class ServerCreated implements ShouldBroadcast, ShouldDispatchAfterCommit
{
use SerializesModels;
}
信息
要了解更多关于解决这些问题的信息,请查阅队列任务和数据库事务的文档。
授权通道
私有通道要求你授权当前认证用户确实可以监听该通道。这是通过向 Laravel 应用发送包含通道名的 HTTP 请求来完成的,让你的应用决定用户是否可以监听该通道。使用 Laravel Echo 时,授权订阅私有通道的 HTTP 请求会自动发出。
在我们的 API 架构中,授权端点是 POST /api/broadcasting/auth(在服务端安装中通过 withBroadcasting() 配置)。Echo 的 authorizer 回调会自动携带 Sanctum Bearer token 发送请求。
定义授权回调
在 routes/channels.php 文件中,用 Broadcast::channel 方法注册通道授权回调:
use App\Models\User;
Broadcast::channel('orders.{orderId}', function (User $user, int $orderId) {
return $user->id === Order::findOrNew($orderId)->user_id;
});
channel() 方法接收两个参数:通道名和一个返回 true 或 false 的回调,表示用户是否被授权监听该通道。
所有授权回调都接收当前认证用户作为第一个参数,任何额外的通配符参数作为后续参数。在这个示例中,我们使用 {orderId} 占位符表示通道名中的「ID」部分是一个通配符。
你可以用 channel:list Artisan 命令查看应用的广播授权回调列表:
php artisan channel:list
授权回调模型绑定
和 HTTP 路由一样,通道路由也可以利用隐式和显式路由模型绑定。例如,你可以请求实际的 Order 模型实例,而不是接收字符串或数字订单 ID:
use App\Models\Order;
use App\Models\User;
Broadcast::channel('orders.{order}', function (User $user, Order $order) {
return $user->id === $order->user_id;
});
注意
与 HTTP 路由模型绑定不同,通道模型绑定不支持自动隐式模型绑定作用域。不过,这很少成为问题,因为大多数通道可以基于单个模型的唯一主键来限定范围。
授权回调认证
私有和在线广播通道通过应用的默认认证 guard 来认证当前用户。如果用户未认证,通道路由会自动被拒绝,授权回调不会被执行。不过,你可以根据需要分配多个自定义 guard 来认证传入请求:
Broadcast::channel('channel', function () {
// ...
}, ['guards' => ['web', 'admin']]);
提示
在纯 API 项目中,withBroadcasting() 已经配置了 auth:sanctum 中间件,所以用户会通过 Sanctum token 认证。上面的 guards 选项适用于需要在同一个应用中支持多种认证方式的场景。
定义通道类
如果你的应用使用很多不同的通道,routes/channels.php 文件可能会变得臃肿。因此,你可以使用通道类来替代闭包授权通道。要生成通道类,用 make:channel Artisan 命令。这个命令会在 App/Broadcasting 目录下放置一个新的通道类:
php artisan make:channel OrderChannel
接下来,在 routes/channels.php 文件中注册你的通道:
use App\Broadcasting\OrderChannel;
Broadcast::channel('orders.{order}', OrderChannel::class);
最后,你可以把通道的授权逻辑放在通道类的 join() 方法中:
<?php
namespace App\Broadcasting;
use App\Models\Order;
use App\Models\User;
class OrderChannel
{
/**
* Create a new channel instance.
*/
public function __construct() {}
/**
* Authenticate the user's access to the channel.
*/
public function join(User $user, Order $order): array|bool
{
return $user->id === $order->user_id;
}
}
信息
和 Laravel 中的许多其他类一样,通道类会由服务容器自动解析。所以,你可以在通道的构造函数中类型提示任何需要的依赖。
广播事件
一旦你定义了事件并标记了 ShouldBroadcast 接口,你只需要用事件的 dispatch 方法触发事件。事件分发器会注意到事件标记了 ShouldBroadcast 接口,并将事件排队等待广播:
use App\Events\OrderShipmentStatusUpdated;
OrderShipmentStatusUpdated::dispatch($order);
只发给其他人
构建使用事件广播的应用时,你可能偶尔需要把事件广播给给定通道的所有订阅者,但排除当前用户。你可以用 broadcast 辅助函数和 toOthers() 方法来实现:
use App\Events\OrderShipmentStatusUpdated;
broadcast(new OrderShipmentStatusUpdated($update))->toOthers();
为了更好地理解什么时候可能想用 toOthers() 方法,假设一个任务列表应用,用户可以通过输入任务名来创建新任务。创建任务时,你的应用会请求一个 API 端点,广播任务的创建并返回新任务的 JSON 表示。当 JavaScript 应用从这个端点收到响应后,它可能直接把新任务插入到任务列表中:
axios.post('/api/task', task)
.then((response) => {
this.tasks.push(response.data);
});
但请记住,我们也广播了任务的创建。如果你的 JavaScript 应用也监听这个事件来添加任务到列表,列表中就会有重复的任务:一个来自端点响应,一个来自广播。你可以用 toOthers() 方法来指示广播器不要把事件广播给当前用户来解决这个问题。
注意
你的事件必须使用 Illuminate\Broadcasting\InteractsWithSockets trait 才能调用 toOthers() 方法。
配置
初始化 Laravel Echo 实例时,会为连接分配一个 socket ID。如果你使用全局 Axios 实例从 JavaScript 应用发起 HTTP 请求,socket ID 会自动作为 X-Socket-ID 头附加到每个发出的请求中。然后,当你调用 toOthers() 方法时,Laravel 会从头中提取 socket ID 并指示广播器不要广播到具有该 socket ID 的任何连接。
如果你不使用全局 Axios 实例,你需要手动配置 JavaScript 应用在所有发出的请求中发送 X-Socket-ID 头。你可以用 Echo.socketId() 方法获取 socket ID:
const socketId = Echo.socketId();
提示
如果你使用 @laravel/echo-vue 的 useSocketId() 组合式 API,可以在任何 Vue 组件中响应式获取当前 socket ID(见 Socket ID)。
自定义连接
如果你的应用与多个广播连接交互,想用非默认的广播器来广播事件,可以用 via() 方法指定将事件推送到哪个连接:
broadcast(new OrderShipmentStatusUpdated($update))->via('reverb');
或者,你可以通过在事件的构造函数中调用 broadcastVia() 方法来指定事件的广播连接。不过在此之前,你应该确保事件类使用了 InteractsWithBroadcasting trait:
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithBroadcasting;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels;
class OrderShipmentStatusUpdated implements ShouldBroadcast
{
use InteractsWithBroadcasting;
public function __construct()
{
$this->broadcastVia('reverb');
}
}
匿名事件
有时你可能想向前端广播一个简单事件,而不创建专门的事件类。为此,Broadcast 门面允许你广播「匿名事件」:
Broadcast::on('orders.'.$order->id)->send();
上面的示例会广播以下事件:
{
"event": "AnonymousEvent",
"data": "[]",
"channel": "orders.1"
}
使用 as() 和 with() 方法,你可以自定义事件的名称和数据:
Broadcast::on('orders.'.$order->id)
->as('OrderPlaced')
->with($order)
->send();
上面的示例会广播如下事件:
{
"event": "OrderPlaced",
"data": "{ id: 1, total: 100 }",
"channel": "orders.1"
}
如果你想在私有通道或在线通道上广播匿名事件,可以使用 private() 和 presence() 方法:
Broadcast::private('orders.'.$order->id)->send();
Broadcast::presence('channels.'.$channel->id)->send();
使用 send() 方法广播匿名事件会将事件分发到应用的队列进行处理。如果你想立即广播事件,可以使用 sendNow() 方法:
Broadcast::on('orders.'.$order->id)->sendNow();
要把事件广播给所有通道订阅者但排除当前认证用户,可以调用 toOthers() 方法:
Broadcast::on('orders.'.$order->id)
->toOthers()
->send();
挽救广播
当应用的队列服务器不可用或 Laravel 在广播事件时遇到错误,通常会抛出异常导致终端用户看到应用错误。由于事件广播通常是应用核心功能的补充,你可以通过在事件上实现 ShouldRescue 接口来防止这些异常中断用户体验。
实现 ShouldRescue 接口的事件在广播尝试时会自动使用 Laravel 的 rescue 辅助函数。这个辅助函数会捕获任何异常,将它们报告给应用的异常处理器进行日志记录,并允许应用继续正常执行而不中断用户的工作流程:
<?php
namespace App\Events;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Contracts\Broadcasting\ShouldRescue;
class ServerCreated implements ShouldBroadcast, ShouldRescue
{
// ...
}
接收广播
监听事件
安装并配置 Laravel Echo 后,你就可以开始监听 Laravel 应用广播的事件了。首先,用 channel() 方法获取公共通道实例,然后调用 listen() 方法监听指定事件:
Echo.channel(`orders.${orderId}`)
.listen('OrderShipmentStatusUpdated', (e) => {
console.log(e.order.name);
});
如果你想监听私有通道上的事件,改用 private() 方法。你可以继续链式调用 listen() 方法在单个通道上监听多个事件:
Echo.private(`orders.${orderId}`)
.listen('OrderShipmentStatusUpdated', (e) => {
console.log(e.order);
})
.listen('OrderShipped', (e) => {
console.log(e.order);
});
停止监听事件
如果你想停止监听给定事件但不离开通道,可以使用 stopListening() 方法:
Echo.private(`orders.${orderId}`)
.stopListening('OrderShipmentStatusUpdated');
离开通道
要离开通道,可以在 Echo 实例上调用 leaveChannel() 方法:
Echo.leaveChannel(`orders.${orderId}`);
如果你想离开通道及其关联的私有和在线通道,可以调用 leave() 方法:
Echo.leave(`orders.${orderId}`);
命名空间
你可能注意到上面的示例中我们没有为事件类指定完整的 App\Events 命名空间。这是因为 Echo 会自动假设事件位于 App\Events 命名空间下。不过,你可以在实例化 Echo 时通过传 namespace 配置选项来配置根命名空间:
configureEcho({
broadcaster: 'reverb',
// ...
namespace: 'App.Other.Namespace',
});
或者,你可以在用 Echo 订阅事件时在事件类前加 .。这让你可以始终指定完全限定的类名:
Echo.channel('orders')
.listen('.App\\Other\\Namespace\\Event', (e) => {
// ...
});
使用 Vue
Laravel Echo 的 @laravel/echo-vue 包提供了 Vue 组合式 API,让监听事件变得轻松。useEcho hook 用于监听私有通道事件,在消费组件卸载时自动离开通道:
<script setup lang="ts">
import { useEcho } from '@laravel/echo-vue';
const orderId = 1;
useEcho(
`orders.${orderId}`,
'OrderShipmentStatusUpdated',
(e) => {
console.log(e.order);
},
);
</script>
你可以通过给 useEcho 提供一个事件数组来监听多个事件:
useEcho(
`orders.${orderId}`,
['OrderShipmentStatusUpdated', 'OrderShipped'],
(e) => {
console.log(e.order);
},
);
你还可以指定广播事件载荷数据的类型,提供更好的类型安全和编辑便利:
type OrderData = {
order: {
id: number;
status: string;
updated_at: string;
};
};
useEcho<OrderData>(`orders.${orderId}`, 'OrderShipmentStatusUpdated', (e) => {
console.log(e.order.id);
console.log(e.order.status);
});
useEcho hook 会在消费组件卸载时自动离开通道;不过,你可以利用返回的函数在需要时手动停止/开始监听通道:
<script setup lang="ts">
import { useEcho } from '@laravel/echo-vue';
const orderId = 1;
const { leaveChannel, leave, stopListening, listen } = useEcho(
`orders.${orderId}`,
'OrderShipmentStatusUpdated',
(e) => {
console.log(e.order);
},
);
// 停止监听但不离开通道
stopListening();
// 重新开始监听
listen();
// 离开通道
leaveChannel();
// 离开通道及其关联的私有和在线通道
leave();
</script>
连接公共通道
要连接公共通道,可以使用 useEchoPublic hook:
<script setup lang="ts">
import { useEchoPublic } from '@laravel/echo-vue';
useEchoPublic('posts', 'PostPublished', (e) => {
console.log(e.post);
});
</script>
连接在线通道
要连接在线通道,可以使用 useEchoPresence hook:
<script setup lang="ts">
import { useEchoPresence } from '@laravel/echo-vue';
useEchoPresence('chat', 'NewMessage', (e) => {
console.log(e.message);
});
</script>
连接状态
你可以用 useConnectionStatus hook 获取当前 WebSocket 连接状态,它提供响应式状态,在连接状态变化时自动更新:
<script setup lang="ts">
import { useConnectionStatus } from '@laravel/echo-vue';
const status = useConnectionStatus();
</script>
<template>
<div>连接状态: {{ status }}</div>
</template>
可能的状态值有:
connected— 成功连接到 WebSocket 服务器connecting— 初始连接尝试进行中reconnecting— 断开连接后尝试重新连接disconnected— 未连接且未尝试重新连接failed— 连接失败且不会重试
Socket ID
你可以用 useSocketId hook 获取当前 WebSocket socket ID,它提供响应式值,在连接用新 socket ID 重新连接时自动更新:
<script setup lang="ts">
import { useSocketId } from '@laravel/echo-vue';
const socketId = useSocketId();
</script>
<template>
<div>Socket ID: {{ socketId }}</div>
</template>
socket ID 的主要用途是配合 toOthers() 方法(见只发给其他人),在发送 HTTP 请求时将 socket ID 作为 X-Socket-ID 头附加到请求中,让 Laravel 知道不要把事件广播给当前用户。
在线通道
在线通道(Presence Channel)建立在私有通道的安全性之上,同时暴露了额外的功能:感知谁订阅了通道。这使得构建强大的协作应用功能变得容易,比如通知用户有其他人正在查看同一页面,或列出聊天室的成员。
授权在线通道
所有在线通道也是私有通道;因此,用户必须获得授权才能访问。不过,在为在线通道定义授权回调时,如果用户被授权加入通道,你不应返回 true,而应返回一个包含用户数据的数组。
授权回调返回的数据将提供给 JavaScript 应用中的在线通道事件监听器。如果用户未被授权加入在线通道,你应该返回 false 或 null:
use App\Models\User;
Broadcast::channel('chat.{roomId}', function (User $user, int $roomId) {
if ($user->canJoinRoom($roomId)) {
return ['id' => $user->id, 'name' => $user->name];
}
});
加入在线通道
要加入在线通道,可以使用 Echo 的 join() 方法。join() 方法会返回一个 PresenceChannel 实现,除了暴露 listen() 方法外,还允许你订阅 here、joining 和 leaving 事件:
Echo.join(`chat.${roomId}`)
.here((users) => {
// 当前在线的所有用户
console.log('在线用户:', users);
})
.joining((user) => {
// 新用户加入
console.log(user.name + ' 加入了房间');
})
.leaving((user) => {
// 用户离开
console.log(user.name + ' 离开了房间');
})
.error((error) => {
console.error(error);
});
here 回调会在成功加入通道后立即执行,并接收一个包含当前订阅该通道的所有其他用户信息的数组。joining 方法会在新用户加入通道时执行,leaving 方法会在用户离开通道时执行。error 方法会在认证端点返回非 200 HTTP 状态码或解析返回的 JSON 出问题时执行。
向在线通道广播
在线通道可以像公共或私有通道一样接收事件。以聊天室为例,我们可能想把 NewMessage 事件广播到房间的在线通道。为此,我们从事件的 broadcastOn() 方法返回一个 PresenceChannel 实例:
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PresenceChannel('chat.'.$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) => {
console.log(e.message);
});
模型广播
注意
在阅读以下关于模型广播的文档之前,我们建议你先熟悉 Laravel 模型广播服务的一般概念,以及如何手动创建和监听广播事件。
当应用的 Eloquent 模型被创建、更新或删除时,广播事件是很常见的。当然,这可以通过手动为 Eloquent 模型状态变化定义自定义事件并标记这些事件为 ShouldBroadcast 来轻松实现。
不过,如果你不在应用中将这些事件用于其他目的,仅为广播而创建事件类可能会很麻烦。为了解决这个问题,Laravel 允许你指定 Eloquent 模型自动广播其状态变化。
首先,你的 Eloquent 模型应该使用 Illuminate\Database\Eloquent\BroadcastsEvents trait。此外,模型应定义一个 broadcastOn() 方法,返回模型事件应该广播到的通道数组:
<?php
namespace App\Models;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Database\Eloquent\BroadcastsEvents;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Post extends Model
{
use BroadcastsEvents, HasFactory;
/**
* Get the user that the post belongs to.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Get the channels that model events should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel|\Illuminate\Database\Eloquent\Model>
*/
public function broadcastOn(string $event): array
{
return [$this, $this->user];
}
}
一旦模型包含这个 trait 并定义了广播通道,当模型实例被创建、更新、删除、软删除或恢复时,它就会开始自动广播事件。
此外,你可能注意到 broadcastOn() 方法接收一个字符串 $event 参数。这个参数包含模型上发生的事件类型,值为 created、updated、deleted、trashed 或 restored。通过检查这个变量的值,你可以确定模型在特定事件下应该广播到哪些通道(如果有的话):
/**
* Get the channels that model events should broadcast on.
*
* @return array<string, array<int, \Illuminate\Broadcasting\Channel|\Illuminate\Database\Eloquent\Model>>
*/
public function broadcastOn(string $event): array
{
return match ($event) {
'deleted' => [],
default => [$this, $this->user],
};
}
自定义模型广播事件创建
有时你可能想自定义 Laravel 如何创建底层模型广播事件。你可以通过在 Eloquent 模型上定义 newBroadcastableEvent() 方法来实现。这个方法应返回一个 Illuminate\Database\Eloquent\BroadcastableModelEventOccurred 实例:
use Illuminate\Database\Eloquent\BroadcastableModelEventOccurred;
/**
* Create a new broadcastable model event for the model.
*/
protected function newBroadcastableEvent(string $event): BroadcastableModelEventOccurred
{
return (new BroadcastableModelEventOccurred(
$this, $event
))->dontBroadcastToCurrentUser();
}
模型广播约定
通道约定
你可能注意到上面模型示例中的 broadcastOn() 方法没有返回 Channel 实例,而是直接返回了 Eloquent 模型。如果模型的 broadcastOn() 方法返回 Eloquent 模型实例(或包含在方法返回的数组中),Laravel 会自动使用模型的类名和主键标识作为通道名为模型实例化一个私有通道实例。
所以,id 为 1 的 App\Models\User 模型会被转换为名为 App.Models.User.1 的 Illuminate\Broadcasting\PrivateChannel 实例。当然,除了从模型的 broadcastOn() 方法返回 Eloquent 模型实例外,你还可以返回完整的 Channel 实例来完全控制模型的通道名:
use Illuminate\Broadcasting\PrivateChannel;
/**
* Get the channels that model events should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(string $event): array
{
return [
new PrivateChannel('user.'.$this->id)
];
}
如果你打算从模型的 broadcastOn() 方法显式返回通道实例,可以将 Eloquent 模型实例传给通道的构造函数。这样做时,Laravel 会使用上面讨论的模型通道约定将 Eloquent 模型转换为通道名字符串:
return [new Channel($this->user)];
如果你需要确定模型的通道名,可以在任何模型实例上调用 broadcastChannel() 方法。例如,对于 id 为 1 的 App\Models\User 模型,这个方法返回字符串 App.Models.User.1:
$user->broadcastChannel();
事件约定
由于模型广播事件不与应用 App\Events 目录中的「实际」事件关联,它们会根据约定被分配名称和载荷。Laravel 的约定是使用模型的类名(不含命名空间)和触发广播的模型事件名来广播事件。
例如,App\Models\Post 模型的更新会向客户端应用广播一个名为 PostUpdated 的事件,载荷如下:
{
"model": {
"id": 1,
"title": "My first post"
},
"socket": "someSocketId"
}
App\Models\User 模型的删除会广播名为 UserDeleted 的事件。
如果你想,可以通过在模型上添加 broadcastAs() 和 broadcastWith() 方法定义自定义广播名称和载荷。这些方法接收正在发生的模型事件/操作名称,允许你为每个模型操作自定义事件名称和载荷。如果 broadcastAs() 方法返回 null,Laravel 会在广播事件时使用上面讨论的模型广播事件名称约定:
/**
* The model event's broadcast name.
*/
public function broadcastAs(string $event): string|null
{
return match ($event) {
'created' => 'post.created',
default => null,
};
}
/**
* Get the data to broadcast for the model.
*
* @return array<string, mixed>
*/
public function broadcastWith(string $event): array
{
return match ($event) {
'created' => ['title' => $this->title],
default => ['model' => $this],
};
}
监听模型广播
为模型添加 BroadcastsEvents trait 并定义模型的 broadcastOn() 方法后,你就可以开始在客户端应用中监听广播的模型事件了。开始之前,你可能想先查阅监听事件的完整文档。
首先,用 private() 方法获取通道实例,然后调用 listen() 方法监听指定事件。通常,传给 private() 方法的通道名应该对应 Laravel 的模型广播约定。
获取通道实例后,你可以用 listen() 方法监听特定事件。由于模型广播事件不与应用 App\Events 目录中的「实际」事件关联,事件名必须以 . 为前缀来表示它不属于特定命名空间。每个模型广播事件都有一个 model 属性,包含模型的所有可广播属性:
Echo.private(`App.Models.User.${userId}`)
.listen('.UserUpdated', (e) => {
console.log(e.model);
});
使用 Vue
如果你使用 Vue,可以使用 Laravel Echo 的 useEchoModel hook 来轻松监听模型广播:
<script setup lang="ts">
import { useEchoModel } from '@laravel/echo-vue';
const userId = 1;
useEchoModel('App.Models.User', userId, ['UserUpdated'], (e) => {
console.log(e.model);
});
</script>
你还可以指定模型事件载荷数据的类型,提供更好的类型安全和编辑便利:
type User = {
id: number;
name: string;
email: string;
};
useEchoModel<User, 'App.Models.User'>('App.Models.User', userId, ['UserUpdated'], (e) => {
console.log(e.model.id);
console.log(e.model.name);
});
客户端事件
有时你可能想把事件广播给其他已连接的客户端,而完全不经过 Laravel 应用。这对于「正在输入」通知等场景特别有用,你想提醒应用的用户有其他人正在给定屏幕上输入消息。
要广播客户端事件,可以使用 Echo 的 whisper() 方法:
<script setup lang="ts">
import { useEcho } from '@laravel/echo-vue';
const roomId = 1;
const userName = '张三';
const { channel } = useEcho(`chat.${roomId}`, ['NewMessage'], (e) => {
console.log('收到消息:', e);
});
// 发送「正在输入」客户端事件
function onTyping() {
channel().whisper('typing', {
name: userName,
});
}
</script>
要监听客户端事件,可以使用 listenForWhisper() 方法:
<script setup lang="ts">
import { useEcho } from '@laravel/echo-vue';
const roomId = 1;
const { channel } = useEcho(`chat.${roomId}`, ['NewMessage'], (e) => {
console.log('收到消息:', e);
});
// 监听其他用户的「正在输入」事件
channel().listenForWhisper('typing', (e) => {
console.log(`${e.name} 正在输入...`);
});
</script>
信息
使用 Reverb 时,客户端事件默认就是开启的,不需要额外配置。如果使用 Pusher Channels,则需要在应用面板中手动启用「Client Events」选项。
通知
将事件广播与通知结合使用,前端可以在通知产生时实时接收,无需刷新页面。开始之前,请确保阅读了使用广播通知通道的文档。
配置通知使用广播通道后,你可以用 Echo 的 notification() 方法监听广播事件。记住,通道名应该与接收通知的实体类名匹配:
Echo.private(`App.Models.User.${userId}`)
.notification((notification) => {
console.log(notification.type);
});
如果你使用 Vue 的 @laravel/echo-vue,可以通过 useEchoModel 获取通道实例后监听通知:
<script setup lang="ts">
import { useEchoModel } from '@laravel/echo-vue';
const userId = 1;
const { channel } = useEchoModel('App.Models.User', userId);
channel().notification((notification) => {
console.log(notification.type);
});
</script>
在这个示例中,通过 broadcast 通道发送给 App\Models\User 实例的所有通知都会被回调接收。App.Models.User.{id} 通道的通道路由授权回调包含在应用的 routes/channels.php 文件中。
停止监听通知
如果你想停止监听通知但不离开通道,可以使用 stopListeningForNotification() 方法:
const callback = (notification) => {
console.log(notification.type);
};
// 开始监听
Echo.private(`App.Models.User.${userId}`)
.notification(callback);
// 停止监听(回调必须是同一个引用)
Echo.private(`App.Models.User.${userId}`)
.stopListeningForNotification(callback);