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

12 阅读4分钟

你希望在原有 SSE 打字机效果的基础上,引入 AbortController 来更优雅地控制 SSE 连接的生命周期,这是一个非常好的优化思路,AbortController 可以让我们主动、精准地终止 SSE 连接和相关异步操作,避免内存泄漏。

实现思路调整

  1. 新增 AbortController 实例管理 SSE 连接的中止信号
  2. 将中止信号(signal)传入 EventSource(需注意兼容性,现代浏览器均支持)
  3. 所有异步操作(如定时器)都关联到中止信号,实现一键终止
  4. 保持原有打字机效果逻辑,仅优化连接控制部分

优化后的完整代码

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 打字机效果(AbortController 版)</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;
        }
        button#abort {
            background-color: #28a745;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="controls">
            <button id="connect">开始连接 SSE</button>
            <button id="pause" disabled>暂停输出</button>
            <button id="abort" disabled>终止连接</button>
            <button id="clear">清空内容</button>
        </div>
        <div id="output"></div>
    </div>

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

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

        // 打字机核心函数:逐字输出
        function typeText() {
            // 检查中止信号,若已中止则停止执行
            if (abortController?.signal.aborted) return;
            
            if (currentIndex < textBuffer.length && !isPaused) {
                outputEl.textContent += textBuffer[currentIndex];
                currentIndex++;
                outputEl.scrollTop = outputEl.scrollHeight;
                // 关联定时器到中止信号
                typingTimer = setTimeout(() => {
                    if (!abortController?.signal.aborted) {
                        typeText();
                    }
                }, 30);
            } else if (currentIndex >= textBuffer.length) {
                clearTimeout(typingTimer);
                pauseBtn.disabled = true;
                abortBtn.disabled = true;
                connectBtn.disabled = false;
            }
        }

        // 建立 SSE 连接(带 AbortController 控制)
        function connectSSE() {
            // 清空之前的状态
            resetState();
            
            try {
                // 创建 AbortController 实例
                abortController = new AbortController();
                const { signal } = abortController;

                // 替换为你的后端 SSE 接口地址
                // 将中止信号传入 EventSource(现代浏览器支持)
                sse = new EventSource('http://localhost:8080/api/ai-stream', { signal });
                
                // 监听中止信号触发事件
                signal.addEventListener('abort', () => {
                    console.log('SSE 连接已通过 AbortController 终止');
                    resetState();
                    connectBtn.disabled = false;
                    pauseBtn.disabled = true;
                    abortBtn.disabled = true;
                });

                // 监听后端推送的消息
                sse.onmessage = function (event) {
                    if (signal.aborted) return; // 已中止则忽略消息
                    
                    const newText = event.data;
                    if (newText) {
                        textBuffer += newText;
                        if (!typingTimer && !isPaused) {
                            typeText();
                        }
                    }
                };

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

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

                // 监听后端自定义的结束事件
                sse.addEventListener('end', function () {
                    console.log('数据推送完成');
                    if (!signal.aborted) {
                        sse.close();
                        abortBtn.disabled = true;
                    }
                });

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

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

        // 暂停/继续输出
        function togglePause() {
            if (abortController?.signal.aborted) return; // 已中止则不操作
            
            isPaused = !isPaused;
            pauseBtn.textContent = isPaused ? '继续输出' : '暂停输出';
            if (!isPaused) {
                typeText();
            }
        }

        // 主动终止 SSE 连接
        function abortConnection() {
            if (abortController) {
                abortController.abort();
            }
        }

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

        // 页面关闭时清理资源
        window.addEventListener('beforeunload', function () {
            if (abortController) {
                abortController.abort();
            }
        });
    </script>
</body>
</html>

关键优化点说明

  1. AbortController 核心使用
    • 创建 abortController = new AbortController() 实例,获取 signal 信号
    • 初始化 EventSource 时传入 { signal },将连接与中止信号绑定
    • 监听 signal.abort 事件,在连接被中止时自动清理资源
  1. 精准的资源控制
    • 新增「终止连接」按钮,点击后调用 abortController.abort() 可立即终止 SSE 连接
    • 所有异步操作(定时器、消息监听)都先检查 signal.aborted 状态,避免无效执行
    • resetState() 函数中优先调用 abortController.abort(),实现一键终止所有相关操作
  1. 兼容性说明
    • EventSource 支持传入 signal 是 ES2022 特性,Chrome 90+、Firefox 102+、Edge 90+ 均支持
    • 若需兼容旧浏览器,可在 abort() 时手动调用 sse.close() 作为兜底

总结

  1. AbortController 替代了原有的手动关闭 SSE 方式,实现了声明式的连接控制,代码更优雅、更符合现代前端规范
  2. 核心逻辑:通过 signal 信号关联 SSE 连接和定时器,调用 abort() 可一键终止所有关联操作,避免内存泄漏
  3. 保留了原有打字机效果和辅助功能,新增「终止连接」按钮,增强了流程控制的灵活性

这个优化版本不仅保留了原有功能,还通过 AbortController 让 SSE 连接的生命周期管理更规范,特别适合在复杂业务场景中使用。