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则是单向推送场景的最优选择之一。

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

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

相关推荐
㳺三才人子6 小时前
初探 Flask
后端·python·flask·html
星栈独行6 小时前
我在 Rust 全栈项目里用 JWT 做无状态认证
开发语言·后端·rust·前端框架·开源·github·web
Java爱好狂.7 小时前
Java程序员体系化学习路线(2026最新版)
java·后端·java面试·java架构师·java程序员·java八股文·java学习路线
陈随易7 小时前
Redis 8.8发布,一定要更新
前端·后端·程序员
装不满的克莱因瓶7 小时前
SpringBoot 如何将 lib 目录中jar包打包进最终的jar包里面
spring boot·后端·maven·jar·mvn
ltl8 小时前
Transformer 原论文实验结果:为什么 28.4 BLEU 足以改写路线图
后端
excel8 小时前
为什么我推荐使用 Termius:现代 SSH 工具的完整体验
前端·后端
卷毛的技术笔记9 小时前
Java后端硬核实战:用Spring AI Alibaba+Redis给LLM装上“超强记忆中枢”
java·人工智能·redis·后端·spring·ai·系统架构
IT_陈寒10 小时前
Java的Optional差点让我掉坑里,这几个坑你别踩
前端·人工智能·后端
子兮曰10 小时前
Harness 驾驭工程深度教程:从 AGENTS.md 到全链路 AI 编码基础设施
前端·后端·ai编程