前端SSE接口数据重复打印问题排查与解决

15 次阅读

前端SSE接口数据重复打印问题排查与解决

Server-Sent Events(SSE)是一种基于 HTTP 协议的服务端推送技术,常用于实现实时消息通知、AI流式输出、日志监控、任务进度推送等场景。相比 WebSocket,SSE 具有实现简单、天然支持 HTTP、自动重连等优势,因此在现代前端项目中被广泛使用。

不过,在实际开发过程中,很多开发者会遇到一个典型问题:SSE接口返回的数据在前端被重复打印,导致消息重复显示、内容重复拼接,甚至出现无限追加的问题。

这种问题表面看起来像是后端重复发送数据,但实际原因可能来自前端事件监听、组件生命周期、连接管理、数据解析方式等多个方面。本文将系统分析 SSE 数据重复打印的常见原因,并提供对应解决方案。

一、SSE数据重复打印的常见表现

SSE 数据重复问题通常表现为以下几种情况:

1. 控制台重复输出相同内容

例如后端推送:

data: hello

data: world

前端代码:

JavaScript
eventSource.onmessage = event => {
    console.log(event.data);
};

实际控制台:

hello
hello
world
world

说明同一个 SSE 消息被多个监听器接收。


2. 页面内容重复追加

例如 AI 对话流式输出:

正常:

你好,我是智能助手

异常:

你好你好,我是智能助手你好,我是智能助手

通常意味着同一条 SSE 数据被多次执行了拼接逻辑。


3. 刷新页面后重复次数增加

第一次进入页面:

消息1

重新进入:

消息1
消息1

第三次:

消息1
消息1
消息1

这种情况高度可能是 SSE 连接没有正确关闭,导致多个连接同时存在。


二、前端SSE数据重复打印的主要原因

1. EventSource被重复创建

这是最常见的问题。

例如 Vue 组件中:

JavaScript
mounted() {
    this.initSSE();
},

methods:{
    initSSE(){
        const source = new EventSource('/api/message');

        source.onmessage = event => {
            console.log(event.data);
        };
    }
}

如果组件多次进入,initSSE() 会执行多次,每次都会创建新的 SSE 连接。

浏览器实际上存在:

连接1 → 接收数据
连接2 → 接收数据
连接3 → 接收数据

最终表现就是:

数据打印3次

解决方案

创建 SSE 前先判断是否已经存在连接:

JavaScript
let eventSource = null;

function createSSE(){

    if(eventSource){
        return;
    }

    eventSource = new EventSource('/api/message');

    eventSource.onmessage = event=>{
        console.log(event.data);
    };
}

2. 组件销毁时没有关闭SSE连接

SPA框架中,页面切换不会刷新浏览器,而是销毁组件。

如果没有关闭:

JavaScript
eventSource.close();

旧连接仍然保持。

例如 Vue:

JavaScript
beforeUnmount(){

    if(this.eventSource){
        this.eventSource.close();
        this.eventSource=null;
    }

}

React:

JavaScript
useEffect(()=>{

    const source=new EventSource(url);

    return ()=>{
        source.close();
    }

},[]);

正确管理生命周期,可以避免大量重复连接。


三、Vue项目中SSE重复打印排查

Vue项目是 SSE 重复问题出现频率较高的场景。

1. 检查mounted是否执行多次

添加日志:

JavaScript
mounted(){

    console.log('SSE初始化');

    this.connectSSE();

}

如果页面打开一次出现:

SSE初始化
SSE初始化
SSE初始化

说明组件被重复挂载。

常见原因:

  • 路由配置重复

  • keep-alive缓存处理不当

  • 父子组件重复调用初始化方法

  • v-if切换导致重新创建组件


2. 避免watch重复触发连接

错误示例:

JavaScript
watch:{
    userId(){

        this.createSSE();

    }
}

如果 userId 多次变化,会不断创建连接。

优化:

JavaScript
watch:{
    userId:{
        handler(){

            this.closeSSE();
            this.createSSE();

        },
        immediate:true
    }
}

3. 使用单例管理SSE连接

大型项目建议统一管理。

例如:

JavaScript
class SSEManager{

    constructor(){
        this.source=null;
    }


    connect(url){

        if(this.source){
            return;
        }

        this.source=new EventSource(url);

        this.source.onmessage=(event)=>{
            console.log(event.data);
        };

    }


    close(){

        if(this.source){
            this.source.close();
            this.source=null;
        }

    }

}

export default new SSEManager();

所有页面共用一个实例,可以避免重复连接。


四、React项目中SSE重复打印原因

React 中常见问题来自 useEffect。

例如:

JavaScript
useEffect(()=>{

    const source=new EventSource(url);

    source.onmessage=e=>{
        console.log(e.data);
    };

},[]);

理论上只执行一次,但开发环境 React StrictMode 会故意执行两次,用于检测副作用问题。

表现:

hello
hello

解决方式

增加清理逻辑:

JavaScript
useEffect(()=>{

    const source=new EventSource(url);

    source.onmessage=e=>{
        console.log(e.data);
    };


    return ()=>{

        source.close();

    }

},[]);

或者确认是否仅开发环境出现:

JavaScript
.StrictMode>

生产环境通常不会重复执行。


五、SSE事件监听重复绑定问题

除了连接重复创建,还可能是事件处理函数绑定多次。

错误:

JavaScript
source.addEventListener(
    'message',
    handler
);


source.addEventListener(
    'message',
    handler
);

同一个事件绑定两个监听器:

message事件
    ↓
handler执行
    ↓
handler执行

解决:

创建前移除旧监听:

JavaScript
source.removeEventListener(
    'message',
    handler
);

或者确保初始化逻辑只执行一次。


六、检查后端是否真的重复发送数据

虽然多数问题来自前端,但也需要排查后端。

可以使用浏览器开发工具:

打开:

F12
→ Network
→ EventStream

查看 SSE 数据流。

正常:

data: hello
data: world

异常:

data: hello
data: hello
data: world
data: world

如果 Network 中已经重复:

说明后端推送逻辑存在问题。

可能原因:

  • 后端循环发送

  • 消息队列消费重复

  • 数据库查询重复

  • 服务端连接没有释放

如果 Network 正常,而 Console 重复:

问题一定在前端。


七、SSE数据解析导致的重复问题

有些项目不是直接使用 EventSource,而是通过 fetch 读取流。

例如:

JavaScript
reader.read().then(({value})=>{
    console.log(
        decoder.decode(value)
    );
});

如果处理方式错误,可能重复解析缓存数据。

推荐:

JavaScript
let buffer='';

reader.read().then(({value,done})=>{

    buffer += decoder.decode(value);

    const messages=buffer.split('

');

    messages.forEach(msg=>{

        if(msg){
            console.log(msg);
        }

    });

});

需要根据 SSE 格式切割数据,而不能简单按 chunk 处理。

因为一次网络读取并不等于一次 SSE 消息。


八、使用AbortController管理SSE请求

如果项目使用 fetch + stream,可以通过 AbortController 主动关闭连接。

示例:

JavaScript
const controller=new AbortController();


fetch('/api/chat',{
    signal:controller.signal
});


//关闭
controller.abort();

页面离开时调用:

JavaScript
onUnmounted(()=>{

    controller.abort();

});

避免后台连接持续存在。


九、SSE重复打印问题快速排查流程

遇到数据重复,可以按照以下顺序定位:

第一步:检查Network

查看 EventStream:

  • 数据是否重复?

  • 是否存在多个连接?

如果多个请求:

说明 EventSource 创建多次。


第二步:检查初始化入口

搜索:

JavaScript
new EventSource

确认调用位置:

  • mounted

  • created

  • useEffect

  • watch

  • 路由切换事件


第三步:检查关闭逻辑

确认是否存在:

JavaScript
close()

或者:

JavaScript
abort()

第四步:检查监听绑定

搜索:

JavaScript
addEventListener

确认没有重复绑定。


第五步:检查数据追加逻辑

例如:

JavaScript
message += event.data;

确认没有被多次执行。


十、SSE最佳实践方案

为了避免 SSE 重复打印问题,推荐采用以下规范:

1. 一个业务保持一个连接

不要每次请求都创建新的 EventSource。


2. 页面销毁及时释放资源

统一处理:

JavaScript
close()

避免连接泄漏。


3. 增加连接状态管理

例如:

JavaScript
{
 status:'connected',
 retry:0,
 source:null
}

方便排查问题。


4. 给消息增加唯一ID

后端返回:

id:10001
data:hello

前端记录:

JavaScript
if(cache.has(id)){
    return;
}

避免重复消费。


5. 生产环境关闭调试日志

开发阶段:

JavaScript
console.log(event.data);

上线后建议移除,避免误判。


总结

前端 SSE 接口数据重复打印问题,通常不是 SSE 技术本身的问题,而是连接管理和生命周期控制不到位导致的。

最常见原因包括:

  • EventSource 被重复创建

  • 页面销毁未关闭连接

  • React StrictMode 导致重复执行

  • Vue 生命周期重复触发

  • 事件监听器重复绑定

  • 流式数据解析错误

  • 后端实际重复推送

排查时应优先查看浏览器 Network 中的 EventStream 数据。如果网络请求本身重复,需要检查后端;如果网络正常但页面重复显示,则重点检查前端连接和状态管理。

通过统一管理 SSE 生命周期、控制连接数量、正确处理组件卸载,可以有效避免数据重复打印问题,保证实时通信功能稳定可靠。