你希望在原有 SSE 打字机效果的基础上,引入 AbortController 来更优雅地控制 SSE 连接的生命周期,这是一个非常好的优化思路,AbortController 可以让我们主动、精准地终止 SSE 连接和相关异步操作,避免内存泄漏。
实现思路调整
- 新增
AbortController实例管理 SSE 连接的中止信号 - 将中止信号(
signal)传入EventSource(需注意兼容性,现代浏览器均支持) - 所有异步操作(如定时器)都关联到中止信号,实现一键终止
- 保持原有打字机效果逻辑,仅优化连接控制部分
优化后的完整代码
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>
关键优化点说明
- AbortController 核心使用:
-
- 创建
abortController = new AbortController()实例,获取signal信号 - 初始化
EventSource时传入{ signal },将连接与中止信号绑定 - 监听
signal.abort事件,在连接被中止时自动清理资源
- 创建
- 精准的资源控制:
-
- 新增「终止连接」按钮,点击后调用
abortController.abort()可立即终止 SSE 连接 - 所有异步操作(定时器、消息监听)都先检查
signal.aborted状态,避免无效执行 resetState()函数中优先调用abortController.abort(),实现一键终止所有相关操作
- 新增「终止连接」按钮,点击后调用
- 兼容性说明:
-
EventSource支持传入signal是 ES2022 特性,Chrome 90+、Firefox 102+、Edge 90+ 均支持- 若需兼容旧浏览器,可在
abort()时手动调用sse.close()作为兜底
总结
AbortController替代了原有的手动关闭 SSE 方式,实现了声明式的连接控制,代码更优雅、更符合现代前端规范- 核心逻辑:通过
signal信号关联 SSE 连接和定时器,调用abort()可一键终止所有关联操作,避免内存泄漏 - 保留了原有打字机效果和辅助功能,新增「终止连接」按钮,增强了流程控制的灵活性
这个优化版本不仅保留了原有功能,还通过 AbortController 让 SSE 连接的生命周期管理更规范,特别适合在复杂业务场景中使用。