博客 / 詳情

返回

FastAdmin框架SSE實時消息推送實現教程

一、前言:什麼是SSE?

SSE(Server-Sent Events,服務器發送事件)是一種基於HTTP的服務器向客户端單向推送實時數據的技術,與WebSocket的雙向通信不同,SSE更適用於服務器向客户端主動推送、客户端僅接收的場景(如實時通知、消息提醒、數據監控等)。

本教程基於FastAdmin(TP5.1內核)實現SSE推送,包含完整的後端接口、前端頁面及交互邏輯,可直接複用並根據業務擴展。

91c93cfdda337274233f7dde800261fd.png

二、核心實現邏輯總覽

SSE實現需滿足兩個核心條件:後端按SSE標準格式輸出數據並維持長連接;前端通過EventSource對象監聽服務器推送事件。整體流程如下:

  1. 後端:創建SSE接口,配置長連接響應頭、禁用緩存,循環推送格式化數據;
  2. 前端:設計消息展示與控制界面(開啓/停止按鈕);
  3. JS:通過EventSource建立連接,監聽服務器事件,處理消息渲染與連接狀態管理。

三、後端實現:控制器SSE接口開發

在FastAdmin的前端控制器(如application/index/controller/Index.php)中添加SSE核心方法與測試頁面方法,代碼分步驟拆解如下。

3.1 完整控制器代碼


<?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();
    }
}

image.png

3.2 代碼分步拆解説明

步驟1:緩存與超時配置(確保實時性)


// 清理已存在的輸出緩存
if (ob_get_level() > 0) {
    ob_end_clean();
}
// 關閉PHP執行超時(SSE需長連接,默認超時會斷開)
set_time_limit(0);

關鍵説明:FastAdmin默認可能開啓輸出緩衝,需清理緩衝確保消息即時推送;set_time_limit(0)取消PHP執行時間限制,避免長連接被強制中斷。

步驟2:SSE核心響應頭(必配項)


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:發送連接初始化事件


echo "event: sse_init\ndata: " . json_encode(['status' => 'success', 'msg' => '連接成功'], JSON_UNESCAPED_UNICODE) . "\n\n";
flush();

SSE標準格式規則:

  • event: 事件名:自定義事件標識(前端需通過對應事件名監聽);
  • data: 數據內容:消息主體,建議用JSON格式;
  • 結尾必須用\n\n(兩個換行)標識一條消息結束;
  • flush():強制刷新輸出緩衝區,確保消息立即發送到客户端。

步驟4:循環推送業務消息


$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,用於展示控制按鈕和實時消息。


<!-- 引入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>
頁面就是這個樣子的

image.png

頁面核心元素説明:

  • 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代碼


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. 核心變量定義

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中添加前端訪問路由(確保頁面和接口可訪問):


// 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秒自動重連)。

截圖

image.png
image.png
image.png
image.png
image.png
我們的鏈接也就sse這一個,它會一直推送通知過來,我們可以設置在有未讀消息的時候再推送,在控制器裏整理好邏輯就行。

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則是單向推送場景的最優選擇之一。

可根據實際業務需求擴展以下功能:用户登錄態校驗、消息已讀/未讀標記、自定義消息類型(如系統通知、訂單提醒)、消息過濾與分頁等。

(注:文檔由網絡乞丐編寫)
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.