使用 SSE(Server-Sent Events,后端适配 SSE 协议)处理后端返回的ai数据,以打字机的形式返回回来

2 阅读4分钟

你想要实现的是通过 SSE(Server-Sent Events)接收后端返回的 AI 数据,并在前端以打字机的逐字输出效果展示出来,这个需求在 AI 对话、实时文本生成场景中非常常见。

实现思路

  1. 建立 SSE 连接,监听后端推送的文本数据
  2. 接收数据后,通过定时器逐字将文本渲染到页面
  3. 处理连接关闭、错误等异常情况,保证体验的完整性
  4. 提供暂停 / 继续、清空等辅助功能(增强实用性)

完整代码实现

以下是可直接运行的前端代码(原生 JavaScript 实现,无框架依赖):

html

预览

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SSE 打字机效果展示</title>
    <style>
        .container {
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }
        #output {
            border: 1px solid #e0e0e0;
            border-radius: 8px;
            padding: 20px;
            min-height: 200px;
            font-size: 16px;
            line-height: 1.6;
            margin: 20px 0;
            white-space: pre-wrap; /* 保留换行和空格 */
        }
        .controls {
            margin-bottom: 20px;
        }
        button {
            padding: 8px 16px;
            margin-right: 10px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            background-color: #007bff;
            color: white;
        }
        button:disabled {
            background-color: #6c757d;
            cursor: not-allowed;
        }
        button#pause {
            background-color: #ffc107;
            color: #333;
        }
        button#clear {
            background-color: #dc3545;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="controls">
            <button id="connect">开始连接 SSE</button>
            <button id="pause" disabled>暂停输出</button>
            <button id="clear">清空内容</button>
        </div>
        <div id="output"></div>
    </div>

    <script>
        // 核心变量
        let sse = null; // SSE 连接实例
        let textBuffer = ''; // 存储后端推送的所有文本
        let currentIndex = 0; // 当前显示到的字符索引
        let typingTimer = null; // 打字机定时器
        let isPaused = false; // 是否暂停输出

        // DOM 元素
        const connectBtn = document.getElementById('connect');
        const pauseBtn = document.getElementById('pause');
        const clearBtn = document.getElementById('clear');
        const outputEl = document.getElementById('output');

        // 打字机核心函数:逐字输出
        function typeText() {
            if (currentIndex < textBuffer.length && !isPaused) {
                // 追加当前字符到输出区域
                outputEl.textContent += textBuffer[currentIndex];
                currentIndex++;
                // 滚动到最底部(避免内容溢出看不到)
                outputEl.scrollTop = outputEl.scrollHeight;
                // 控制打字速度(可根据需求调整,单位:毫秒)
                typingTimer = setTimeout(typeText, 30);
            } else if (currentIndex >= textBuffer.length) {
                // 文本全部输出完成,清理定时器
                clearTimeout(typingTimer);
                pauseBtn.disabled = true;
                connectBtn.disabled = false;
            }
        }

        // 建立 SSE 连接
        function connectSSE() {
            // 清空之前的状态
            resetState();
            
            try {
                // 替换为你的后端 SSE 接口地址
                sse = new EventSource('http://localhost:8080/api/ai-stream');
                
                // 监听后端推送的消息(默认事件名 message)
                sse.onmessage = function (event) {
                    // 后端推送的文本内容(需确保后端返回的是纯文本)
                    const newText = event.data;
                    if (newText) {
                        // 将新文本追加到缓冲区
                        textBuffer += newText;
                        // 如果还没开始打字,启动打字机
                        if (!typingTimer && !isPaused) {
                            typeText();
                        }
                    }
                };

                // 监听连接打开事件
                sse.onopen = function () {
                    console.log('SSE 连接已建立');
                    connectBtn.disabled = true;
                    pauseBtn.disabled = false;
                };

                // 监听错误事件
                sse.onerror = function (error) {
                    console.error('SSE 连接错误:', error);
                    // 连接异常时关闭 SSE
                    if (sse.readyState === EventSource.CLOSED) {
                        resetState();
                        connectBtn.disabled = false;
                        pauseBtn.disabled = true;
                        alert('SSE 连接已断开,请重试');
                    }
                };

                // 监听后端自定义的结束事件(可选,需后端配合)
                sse.addEventListener('end', function () {
                    console.log('数据推送完成');
                    sse.close();
                });

            } catch (error) {
                console.error('创建 SSE 连接失败:', error);
                alert('创建 SSE 连接失败,请检查接口地址或网络');
            }
        }

        // 重置所有状态
        function resetState() {
            // 关闭现有 SSE 连接
            if (sse) {
                sse.close();
                sse = null;
            }
            // 清理定时器
            clearTimeout(typingTimer);
            typingTimer = null;
            // 重置变量
            textBuffer = '';
            currentIndex = 0;
            isPaused = false;
            // 重置 UI
            outputEl.textContent = '';
            pauseBtn.textContent = '暂停输出';
            pauseBtn.disabled = true;
        }

        // 暂停/继续输出
        function togglePause() {
            isPaused = !isPaused;
            pauseBtn.textContent = isPaused ? '继续输出' : '暂停输出';
            // 如果从暂停切换为继续,重新启动打字机
            if (!isPaused) {
                typeText();
            }
        }

        // 绑定按钮事件
        connectBtn.addEventListener('click', connectSSE);
        pauseBtn.addEventListener('click', togglePause);
        clearBtn.addEventListener('click', resetState);

        // 页面关闭时清理 SSE 连接(避免内存泄漏)
        window.addEventListener('beforeunload', function () {
            if (sse) {
                sse.close();
            }
        });
    </script>
</body>
</html>

代码关键说明

  1. SSE 连接核心
    • 使用 new EventSource('后端接口地址') 建立 SSE 长连接
    • onmessage 监听后端推送的消息,将文本存入 textBuffer 缓冲区
    • onopen/onerror 处理连接状态,保证异常时能及时重置
  1. 打字机效果
    • typeText() 函数通过 setTimeout 逐字读取 textBuffer 并渲染
    • 可调整 setTimeout 的延迟时间(当前 30ms)控制打字速度,值越小速度越快
    • scrollTop = scrollHeight 保证内容始终滚动到最新位置
  1. 辅助功能
    • 暂停 / 继续:通过 isPaused 标记控制定时器执行
    • 清空:调用 resetState() 重置所有状态和 UI
    • 页面卸载时关闭 SSE 连接,避免无效长连接

后端适配说明(关键)

前端代码需要后端配合遵循 SSE 协议返回数据,后端需满足:

  1. 响应头必须包含:plaintext
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
  1. 数据格式必须是 SSE 规范格式(每行以 data: 开头,空行结束):plaintext
data: 你好

data: 世界

data: [DONE]  // 自定义结束标识
  1. 不要一次性返回所有文本,需分批次推送(模拟 AI 生成过程)

总结

  1. 核心逻辑:SSE 接收文本存入缓冲区 → 定时器逐字渲染缓冲区内容,实现打字机效果
  2. 关键配置:可调整 setTimeout 延迟时间控制打字速度,替换 EventSource 地址为实际后端接口
  3. 异常处理:包含连接错误、页面卸载、输出完成等边界情况,保证功能稳定性

该代码可直接运行,只需替换后端接口地址即可适配你的业务场景,同时保留了暂停、清空等实用功能,满足日常使用需求。