FastAdmin框架SSE实时消息推送实现教程

FastAdmin框架SSE实时消息推送实现教程

一、前言:什么是SSE?

SSE(Server-Sent Events,服务器发送事件)是一种基于HTTP的服务器向客户端单向推送实时数据的技术,与WebSocket的双向通信不同,SSE更适用于服务器向客户端主动推送、客户端仅接收的场景(如实时通知、消息提醒、数据监控等)。

本教程基于FastAdmin(TP5.1内核)实现SSE推送,包含完整的后端接口、前端页面及交互逻辑,可直接复用并根据业务扩展。

二、核心实现逻辑总览

SSE实现需满足两个核心条件:后端按SSE标准格式输出数据并维持长连接;前端通过EventSource对象监听服务器推送事件。整体流程如下:

  1. 后端:创建SSE接口,配置长连接响应头、禁用缓存,循环推送格式化数据;

  2. 前端:设计消息展示与控制界面(开启/停止按钮);

  3. 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 测试步骤

  1. 启动FastAdmin项目,访问测试页面:http://你的域名/index/test

  2. 点击「开启实时通知」按钮,状态变为「已连接(实时接收消息)」;

  3. 消息容器中每2秒会新增一条实时消息,控制台可查看调试日志;

  4. 点击「停止实时通知」按钮,连接断开,状态变为「已停止(需重新开启)」;

  5. 若关闭页面再重新打开,会自动恢复连接(异常断开后3秒自动重连)。

截图

5.3 生产环境注意事项

  1. 跨域配置:将控制器中Access-Control-Allow-Origin: *替换为你的前端域名(如https://admin.xxx.com),避免跨域安全风险;

  2. Nginx配置:确保Nginx禁用缓冲,可在站点配置中添加:proxy_buffering off;,与后端X-Accel-Buffering: no配合使用;

  3. 连接限制:SSE基于HTTP长连接,需根据服务器配置调整最大并发连接数(如Nginx的worker_connections);

  4. 业务优化:将模拟数据替换为Redis/消息队列查询,避免数据库频繁查询;可根据用户ID过滤消息(需结合登录状态,在接口中添加用户认证);

  5. 推送次数:根据业务需求调整$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则是单向推送场景的最优选择之一。

可根据实际业务需求扩展以下功能:用户登录态校验、消息已读/未读标记、自定义消息类型(如系统通知、订单提醒)、消息过滤与分页等。

(注:文档由网络乞丐编写)

相关推荐
悟空码字8 小时前
SpringBoot动态脱敏实战,从注解到AOP的优雅打码术
java·后端
小鸡脚来咯8 小时前
springboot项目包结构
java·spring boot·后端
爱学习的小可爱卢8 小时前
JavaEE进阶——SpringBoot日志从入门到精通
java·spring boot·后端
Clarence Liu9 小时前
Go Context 深度解析:从源码到 RESTful 框架的最佳实践
开发语言·后端·golang
踏浪无痕9 小时前
Nacos到底是AP还是CP?一文说清楚
分布式·后端·面试
踏浪无痕9 小时前
深入JRaft:Nacos配置中心的性能优化实践
分布式·后端·面试
我梦见我梦见我9 小时前
CentOS下安装RocketMQ
后端
Cache技术分享9 小时前
273. Java Stream API - Stream 中的中间操作:Mapping 操作详解
前端·后端
天天摸鱼的java工程师9 小时前
Docker+K8s 部署微服务:从搭建到运维的全流程指南(Java 老鸟实战版)
java·后端