FastAdmin框架SSE实时消息推送实现教程
一、前言:什么是SSE?
SSE(Server-Sent Events,服务器发送事件)是一种基于HTTP的服务器向客户端单向推送实时数据的技术,与WebSocket的双向通信不同,SSE更适用于服务器向客户端主动推送、客户端仅接收的场景(如实时通知、消息提醒、数据监控等)。
本教程基于FastAdmin(TP5.1内核)实现SSE推送,包含完整的后端接口、前端页面及交互逻辑,可直接复用并根据业务扩展。

二、核心实现逻辑总览
SSE实现需满足两个核心条件:后端按SSE标准格式输出数据并维持长连接;前端通过EventSource对象监听服务器推送事件。整体流程如下:
-
后端:创建SSE接口,配置长连接响应头、禁用缓存,循环推送格式化数据;
-
前端:设计消息展示与控制界面(开启/停止按钮);
-
JS:通过
EventSource建立连接,监听服务器事件,处理消息渲染与连接状态管理。
三、后端实现:控制器SSE接口开发
在FastAdmin的前端控制器(如application/index/controller/Index.php)中添加SSE核心方法与测试页面方法,代码分步骤拆解如下。
3.1 完整控制器代码
php
<?php
namespace app\index\controller;
use app\common\controller\Frontend;
class Index extends Frontend
{
/**
* 前台 SSE 消息推送接口
* 支持匿名访问(也可根据业务要求强制登录)
*/
public function sse()
{
// 1. 清理并禁用输出缓存,确保消息实时性
if (ob_get_level() > 0) {
ob_end_clean();
}
// 关闭PHP执行超时,维持长连接
set_time_limit(0);
// 2. 设置SSE核心响应头(FastAdmin/TP5.1通用)
header('Content-Type: text/event-stream'); // SSE专属MIME类型
header('Cache-Control: no-cache'); // 禁止缓存
header('Connection: keep-alive'); // 保持长连接
header('X-Accel-Buffering: no'); // 禁用Nginx缓冲(生产必加)
header('Access-Control-Allow-Origin: *'); // 跨域支持(生产替换为具体域名)
header('Access-Control-Allow-Methods: GET');
header('Access-Control-Allow-Headers: Content-Type');
// 3. 发送初始化事件(告知客户端连接成功)
echo "event: sse_init\ndata: " . json_encode(['status' => 'success', 'msg' => '连接成功'], JSON_UNESCAPED_UNICODE) . "\n\n";
flush();
// 4. 循环推送消息(核心逻辑)
$count = 0;
$maxCount = 50; // 最大推送次数,避免无限循环
while (true) {
// 检测客户端断开连接或达到最大次数,终止循环
if (connection_aborted() || $count >= $maxCount) {
break;
}
// 模拟业务数据(可替换为数据库/Redis/MQ查询)
$data = [
'id' => $count + 1,
'title' => 'FastAdmin实时通知',
'content' => '新消息:' . date('Y-m-d H:i:s'),
'time' => date('H:i:s'),
'url' => '/index/sse/detail'
];
// 按SSE标准格式输出(event指定事件名,data为消息体)
echo "event: my_event\ndata: " . json_encode($data, JSON_UNESCAPED_UNICODE) . "\n\n";
// 强制刷新缓冲区,确保消息立即推送
flush();
// 控制推送频率(每2秒1条,可根据业务调整)
sleep(2);
$count++;
}
// 5. 清理资源
ob_clean();
return;
}
/**
* SSE测试页面渲染方法
*/
public function test()
{
return $this->view->fetch();
}
}

3.2 代码分步拆解说明
步骤1:缓存与超时配置(确保实时性)
php
// 清理已存在的输出缓存
if (ob_get_level() > 0) {
ob_end_clean();
}
// 关闭PHP执行超时(SSE需长连接,默认超时会断开)
set_time_limit(0);
关键说明:FastAdmin默认可能开启输出缓冲,需清理缓冲确保消息即时推送;set_time_limit(0)取消PHP执行时间限制,避免长连接被强制中断。
步骤2:SSE核心响应头(必配项)
php
header('Content-Type: text/event-stream'); // 告诉浏览器这是SSE流
header('Cache-Control: no-cache'); // 禁止浏览器缓存推送内容
header('Connection: keep-alive'); // 启用HTTP长连接
header('X-Accel-Buffering: no'); // 禁用Nginx代理缓冲(生产环境必须加,否则消息会延迟)
header('Access-Control-Allow-Origin: *'); // 跨域配置(开发环境用*,生产替换为你的域名如https://xxx.com)
关键说明:X-Accel-Buffering: no是生产环境核心配置,Nginx默认会缓冲输出内容,导致消息无法实时推送,必须禁用。
步骤3:发送连接初始化事件
php
echo "event: sse_init\ndata: " . json_encode(['status' => 'success', 'msg' => '连接成功'], JSON_UNESCAPED_UNICODE) . "\n\n";
flush();
SSE标准格式规则:
-
event: 事件名:自定义事件标识(前端需通过对应事件名监听); -
data: 数据内容:消息主体,建议用JSON格式; -
结尾必须用
\n\n(两个换行)标识一条消息结束; -
flush():强制刷新输出缓冲区,确保消息立即发送到客户端。
步骤4:循环推送业务消息
php
$count = 0;
$maxCount = 50; // 限制最大推送次数,避免服务器资源浪费
while (true) {
// 退出条件:客户端断开连接 或 达到最大推送次数
if (connection_aborted() || $count >= $maxCount) {
break;
}
// 1. 业务逻辑:查询数据库/Redis/MQ获取真实数据(此处为模拟)
$data = [
'id' => $count + 1,
'title' => 'FastAdmin实时通知',
'content' => '新消息:' . date('Y-m-d H:i:s'),
'time' => date('H:i:s'),
'url' => '/index/sse/detail' // 消息详情页地址
];
// 2. 按SSE格式输出消息(事件名my_event,前端对应监听)
echo "event: my_event\ndata: " . json_encode($data, JSON_UNESCAPED_UNICODE) . "\n\n";
flush();
// 3. 控制推送频率(每2秒1条,可根据业务调整)
sleep(2);
$count++;
}
关键说明:connection_aborted()用于检测客户端是否主动断开连接(如关闭页面),避免服务器空循环;实际开发中需将模拟数据替换为真实业务查询(如查询未读消息表)。
四、前端实现:页面与交互逻辑
前端包含两部分:页面结构(HTML)和交互逻辑(JS),需放在FastAdmin对应的视图与JS目录中。
4.1 前端页面(HTML)
路径:application/index/view/index/test.html,用于展示控制按钮和实时消息。
html
<!-- 引入FastAdmin公共资源(无需修改) -->
<!-- 前台页面内容 -->
<div class="container">
<h2>我的实时消息</h2>
<!-- 新增:拆分开启/停止两个独立按钮 -->
<div style="margin: 10px 0; display: flex; gap: 10px;">
<button id="sse-start-btn" class="layui-btn layui-btn-normal" style="padding: 6px 15px;">
开启实时通知
</button>
<button id="sse-stop-btn" class="layui-btn layui-btn-danger" style="padding: 6px 15px; opacity: 0.5; cursor: not-allowed;">
停止实时通知
</button>
<span id="sse-status" style="margin-left: 10px; color: #999; align-self: center;">未连接</span>
</div>
<!-- 消息展示区域 -->
<div id="msg-container" style="width: 100%; max-width: 600px; height: 400px; border: 1px solid #eee; padding: 10px; overflow-y: auto; margin-top: 20px;"></div>
</div>
页面核心元素说明:
-
sse-start-btn:开启SSE连接按钮; -
sse-stop-btn:停止SSE连接按钮(默认禁用); -
sse-status:显示连接状态(未连接/已连接/已停止); -
msg-container:实时消息渲染容器。
4.2 交互逻辑(JS)
路径:public/assets/js/frontend/index.js,核心是通过EventSource与后端建立连接,处理消息与状态。
4.2.1 完整JS代码
javascript
define(['jquery', 'bootstrap', 'frontend', 'form', 'template'], function ($, undefined, Frontend, Form, Template) {
var Controller = {
test: function () {
// ========== SSE核心变量 ==========
let eventSource = null; // EventSource实例(SSE连接核心)
let isSSEConnected = false; // 连接状态标记
let isManuallyStopped = false; // 手动停止标记(区分"手动停止"和"异常断开")
// ========== 核心方法 ==========
/**
* 关闭SSE连接
* @param {boolean} forceStop - 是否为手动停止
*/
function closeSSE(forceStop = false) {
if (eventSource) {
eventSource.close(); // 关闭连接
eventSource = null;
isSSEConnected = false;
if (forceStop) {
isManuallyStopped = true; // 标记为手动停止,避免自动重连
}
updateSSEUI(); // 更新按钮与状态UI
}
}
/**
* 初始化SSE连接
*/
function initSSE() {
// 避免重复连接:已连接 或 手动停止后不允许重复初始化
if (isSSEConnected || isManuallyStopped) return;
closeSSE(); // 确保之前的连接已关闭
isManuallyStopped = false;
// 后端SSE接口地址(需与控制器路由一致)
const sseUrl = '/index/index/sse';
try {
// 1. 创建EventSource实例,建立连接
eventSource = new EventSource(sseUrl);
isSSEConnected = true;
updateSSEUI(); // 初始化后立即更新UI
// 2. 监听后端"连接成功"事件(对应后端的sse_init事件)
eventSource.addEventListener('sse_init', function(e) {
const res = JSON.parse(e.data);
console.log('SSE连接成功:', res);
$('#sse-status').text('已连接(实时接收消息)').css('color', '#009688');
});
// 3. 监听后端"业务消息"事件(对应后端的my_event事件,核心!)
eventSource.addEventListener('my_event', function(e) {
const data = JSON.parse(e.data); // 解析后端推送的JSON数据
console.log('收到业务消息:', data);
renderMsg(data); // 渲染消息到页面
});
// 4. 监听连接错误(异常断开时触发)
eventSource.onerror = function(err) {
console.error('SSE连接错误:', err);
isSSEConnected = false;
updateSSEUI();
// 非手动停止的异常断开,3秒后自动重连
if (!isManuallyStopped) {
closeSSE();
setTimeout(initSSE, 3000);
}
};
} catch (err) {
console.error('初始化SSE失败:', err);
// 非手动停止的失败,5秒后重试
if (!isManuallyStopped) {
setTimeout(initSSE, 5000);
}
}
}
/**
* 渲染消息到页面
* @param {object} data - 后端推送的消息数据
*/
function renderMsg(data) {
const msgContainer = $('#msg-container')[0];
// 创建消息DOM元素(使用layui风格样式)
const msgItem = document.createElement('div');
msgItem.style = 'padding: 8px; margin: 5px 0; background: #f9f9f9; border-radius: 4px;';
// 消息内容拼接(可根据需求修改样式)
msgItem.innerHTML = `
<div><strong>${data.title}</strong> <small style="color: #999;">${data.time}</small></div>
<div style="margin-top: 5px;">${data.content}</div>
<div style="margin-top: 5px;"><a href="${data.url}" style="color: #009688;">查看详情</a></div>
`;
// 添加到消息容器并自动滚动到底部
msgContainer.appendChild(msgItem);
msgContainer.scrollTop = msgContainer.scrollHeight;
}
/**
* 更新UI状态(按钮禁用/启用 + 状态文字)
*/
function updateSSEUI() {
const $startBtn = $('#sse-start-btn');
const $stopBtn = $('#sse-stop-btn');
const $status = $('#sse-status');
if (isSSEConnected && !isManuallyStopped) {
// 已连接状态:禁用开启按钮,启用停止按钮
$startBtn.prop('disabled', true).css({opacity: 0.5, cursor: 'not-allowed'});
$stopBtn.prop('disabled', false).css({opacity: 1, cursor: 'pointer'});
$status.text('已连接(实时接收消息)').css('color', '#009688');
} else {
// 未连接/已停止状态:启用开启按钮,禁用停止按钮
$startBtn.prop('disabled', false).css({opacity: 1, cursor: 'pointer'});
$stopBtn.prop('disabled', true).css({opacity: 0.5, cursor: 'not-allowed'});
if (isManuallyStopped) {
$status.text('已停止(需重新开启)').css('color', '#FF5722');
} else {
$status.text('未连接(点击开启通知)').css('color', '#999');
}
}
}
// ========== 事件绑定 ==========
$(function() {
// 开启SSE连接按钮点击事件
$('#sse-start-btn').off('click').on('click', function() {
if (!isSSEConnected && !isManuallyStopped) {
initSSE();
}
});
// 停止SSE连接按钮点击事件
$('#sse-stop-btn').off('click').on('click', function() {
closeSSE(true); // 传入true标记为手动停止
});
});
// ========== 页面关闭时清理 ==========
// 页面刷新/关闭前,主动断开SSE连接,释放服务器资源
$(window).on('beforeunload', function() {
closeSSE();
});
},
};
return Controller;
});
4.2.2 JS核心逻辑拆解
1. 核心变量定义
javascript
let eventSource = null; // EventSource实例(SSE连接的核心对象)
let isSSEConnected = false; // 标记是否处于连接状态
let isManuallyStopped = false; // 标记是否为用户手动停止(避免异常重连)
2. 连接管理方法
-
initSSE():初始化连接,创建EventSource实例,监听后端3类事件(连接成功、业务消息、连接错误); -
closeSSE():关闭连接,更新状态标记,避免异常重连; -
updateSSEUI():根据连接状态同步按钮禁用/启用状态和状态文字,提升用户体验。
3. 消息渲染逻辑
renderMsg()方法负责将后端推送的JSON数据转化为页面DOM元素,核心功能:
-
创建符合Layui风格的消息卡片;
-
拼接消息标题、内容、时间和详情链接;
-
添加消息到容器后自动滚动到底部,确保用户看到最新消息。
五、部署与测试
5.1 路由配置(FastAdmin直接不写了,按路径去访问)
在route/route.php中添加前端访问路由(确保页面和接口可访问):
php
// SSE测试页面路由
Route::get('index/test', 'index/index/test');
// SSE推送接口路由
Route::get('index/sse', 'index/index/sse');
5.2 测试步骤
-
启动FastAdmin项目,访问测试页面:
http://你的域名/index/test; -
点击「开启实时通知」按钮,状态变为「已连接(实时接收消息)」;
-
消息容器中每2秒会新增一条实时消息,控制台可查看调试日志;
-
点击「停止实时通知」按钮,连接断开,状态变为「已停止(需重新开启)」;
-
若关闭页面再重新打开,会自动恢复连接(异常断开后3秒自动重连)。
截图





5.3 生产环境注意事项
-
跨域配置:将控制器中
Access-Control-Allow-Origin: *替换为你的前端域名(如https://admin.xxx.com),避免跨域安全风险; -
Nginx配置:确保Nginx禁用缓冲,可在站点配置中添加:
proxy_buffering off;,与后端X-Accel-Buffering: no配合使用; -
连接限制:SSE基于HTTP长连接,需根据服务器配置调整最大并发连接数(如Nginx的
worker_connections); -
业务优化:将模拟数据替换为Redis/消息队列查询,避免数据库频繁查询;可根据用户ID过滤消息(需结合登录状态,在接口中添加用户认证);
-
推送次数:根据业务需求调整
$maxCount(最大推送次数),或移除次数限制(需确保有可靠的退出条件)。
六、常见问题排查
| 问题现象 | 排查方向 |
|---|---|
| 点击开启按钮无反应,控制台无日志 | 1. 检查JS路径是否正确引入;2. 确认sseUrl与路由配置一致;3. 查看浏览器控制台「网络」面板,是否有SSE接口请求 |
| 消息延迟推送或批量推送 | 1. 确认后端添加X-Accel-Buffering: no响应头;2. 检查Nginx是否配置proxy_buffering off;;3. 确保代码中每次输出后调用flush() |
| 连接频繁断开,自动重连无效 | 1. 检查服务器是否开启防火墙/安全组限制;2. 确认PHPset_time_limit(0)已配置;3. 查看服务器日志,是否有内存溢出或进程被杀情况 |
| 跨域错误 | 1. 检查后端跨域响应头是否配置;2. 确保前端域名与Access-Control-Allow-Origin一致;3. 确认请求方法为GET(SSE仅支持GET) |
七、总结
本教程基于FastAdmin实现了轻量级的SSE实时推送功能,核心优势在于:无需引入额外组件,基于HTTP协议实现,开发成本低,适用于消息通知、数据监控等单向推送场景。如需双向通信(如聊天功能),可考虑WebSocket技术,而SSE则是单向推送场景的最优选择之一。
可根据实际业务需求扩展以下功能:用户登录态校验、消息已读/未读标记、自定义消息类型(如系统通知、订单提醒)、消息过滤与分页等。
(注:文档由网络乞丐编写)