今天实战的SSE协议,这个协议是基于HTTP的一个轻量级单向传输协议,允许服务器主动向客户端推送实时数据,场景主要有:新闻推送、消息通知、股票行情、实时日志等。

核心特性如下:
1、单向通信
2、基于HTTP
3、长连接(替代轮询)
4、自动重连

1.客户端基本使用方法

这里简单画了个流程图表示生命周期。
1、先是连接成功触发open事件;
2、然后接收message消息是要配置监听的,建议用addEventListener,因为如果使用onMessage无法接收指定消息类型,主要就是后面服务端推送的时候会指定类型。前端接收的都是字符串类型,注意后端如果是json格式要用JSON.parse进行转换;
3、网络中断,服务器出错都会触发error事件。
4、关闭连接的close方法,通常离开页面就要关闭
在这里插入图片描述

//url为后端sse服务器地址,根据地址创建连接
const eventSource = new EventSource(url);
// 建立连接触发open事件
eventSource.onopen = () => {
console.log('✅ 触发open事件,SSE连接已建立');
};
// 方式1:使用onmessage属性
eventSource.onmessage = function (event) {
// event.data为服务器推送的文本数据
var data = event.data;
console.log('收到数据:', data);
// 可在此处处理数据,如更新页面内容
};
// 方式2:使用addEventListener,这里如果是message就是和onmessage用法一样
//如果是order、buy就可以自定义监听多种类型,把下面的message替换成自己后台的事件名称
eventSource.addEventListener('message', function (event) {
var data = event.data;
console.log('收到数据(监听方式):', data);
}, false);
// 异常触发error事件
eventSource.onerror = (error) => {
console.error('❌ 触发error事件,SSE连接错误:', error);
};
// 主动关闭SSE连接
eventSource.close();
console.log('SSE连接已手动关闭');

2.服务器端使用方法

先要了解服务端的实现规范,主要从三个方面入手:http头信息要求、数据传输格式、核心字段。

2.1HTTP 头信息要求

Content-Type: text/event-stream // 必须,指定为事件流类型
Cache-Control: no-cache // 必须,禁止缓存,确保数据实时性
Connection: keep-alive // 必须,保持长连接

2.2数据传输格式

1、每行格式为[字段]: 值\n(字段名后必须跟冒号和空格,结尾用换行符\n)
2、多条消息之间用\n\n(两个换行符)分隔。
3、此外,以:开头的行是注释(服务器可定期发送注释保持连接)。
*换行符必须是\n(Unix格式),\r\n可能导致客户端解析错误。

: 这是注释(客户端会忽略)\n
data: 这是第1条消息\n\n
data: 这是第2条消息的第一行\n
data: 这是第3条消息的第二行\n\n

2.3核心字段说明

data字段:消息内容
event字段:指定事件类型
id字段:消息标识,发给谁
retry字段:重连间隔

3.服务端实现

sse在springboot项目中,spring-boot-starter-web提供了SSE核心类SseEmitter。

3.1简单实现

下面是一个简单的实现方式,创建一个接口,供前端访客,建立sse长连接。然后提供了一个广播接口,只要调用就像所有客户端发送消息。还有一个模拟进度通知接口,定时向所有客户端通知进度。这里简单描述下框架,首先要有个全局的SseEmitter列表,只要有客户端连接就存入列表,所有连接的客户端都存在这里。这样存在的问题是不能定向推送,场景有局限。

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author 馒头
*/
@RestController
public class SseController {
// 存储所有活跃的SSE连接(线程安全的列表)
// CopyOnWriteArrayList适合读多写少场景,避免并发问题
private final CopyOnWriteArrayList<SseEmitter> emitters = new CopyOnWriteArrayList<>();
  // 线程池:用于异步发送事件,避免阻塞主线程
  private final ExecutorService executor = Executors.newCachedThreadPool();
  /**
  * 客户端订阅SSE的接口
  * 客户端通过访问该接口建立长连接,接收服务器推送的事件
  */
  @GetMapping(value = "/sse/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
  public SseEmitter subscribe() {
  // 创建SseEmitter实例,设置超时时间为无限(默认30秒会超时,这里设为Long.MAX_VALUE避免自动断开)
  SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
  // 将新连接加入活跃列表(后续推送消息时会遍历这个列表)
  emitters.add(emitter);
  // 设置连接完成/超时的回调:从活跃列表中移除该连接,释放资源
  emitter.onCompletion(() -> emitters.remove(emitter)); // 连接正常关闭
  emitter.onTimeout(() -> emitters.remove(emitter));     // 连接超时关闭
  emitter.onError((e) -> emitters.remove(emitter));     // 异常关闭
  // 发送初始连接成功消息(给客户端的"欢迎消息")
  try {
  emitter.send(SseEmitter.event()
  .name("CONNECTED")  // 事件名称:客户端可通过"CONNECTED"事件监听
  .data("You are successfully connected to SSE server!")  // 消息内容
  .reconnectTime(5000)); // 告诉客户端:如果断开连接,5秒后重连
  } catch (IOException e) {
  // 发送失败时,标记连接异常结束
  emitter.completeWithError(e);
  }
  return emitter; // 将emitter返回给客户端,保持连接
  }
  /**
  * 广播消息接口:向所有已连接的客户端推送消息
  * 可通过浏览器访问 http://localhost:项目端口/sse/broadcast?message=xxx 触发
  */
  @GetMapping("/sse/broadcast")
  public String broadcastMessage(@RequestParam String message) {
  // 用线程池异步执行广播,避免阻塞当前请求
  executor.execute(() -> {
  // 遍历所有活跃连接,逐个发送消息
  for (SseEmitter emitter : emitters) {
  try {
  emitter.send(SseEmitter.event()
  .name("BROADCAST")  // 事件名称:客户端监听"BROADCAST"事件
  .data(message)      // 广播的消息内容
  .id(String.valueOf(System.currentTimeMillis()))); // 消息ID(用于重连时定位)
  } catch (IOException e) {
  // 发送失败(可能客户端已断开),从列表中移除并标记连接结束
  emitters.remove(emitter);
  emitter.completeWithError(e);
  }
  }
  });
  return "Broadcast message: " + message; // 给调用者的响应
  }
  /**
  * 模拟长时间任务:向客户端推送实时进度
  * 适合文件上传、数据处理等需要实时反馈进度的场景
  */
  @GetMapping("/sse/start-task")
  public String startTask() {
  // 异步执行任务,避免阻塞当前请求
  executor.execute(() -> {
  try {
  // 模拟任务进度:从0%到100%,每次增加10%
  for (int i = 0; i <= 100; i += 10) {
  Thread.sleep(1000); // 休眠1秒,模拟处理耗时
  // 向所有客户端推送当前进度
  for (SseEmitter emitter : emitters) {
  try {
  emitter.send(SseEmitter.event()
  .name("PROGRESS")  // 事件名称:客户端监听"PROGRESS"事件
  .data(i + "% completed")  // 进度数据
  .id("task-progress")); // 固定ID,标识这是任务进度消息
  } catch (IOException e) {
  // 发送失败,移除连接
  emitters.remove(emitter);
  }
  }
  // 任务完成时,发送结束消息
  if (i == 100) {
  for (SseEmitter emitter : emitters) {
  try {
  emitter.send(SseEmitter.event()
  .name("COMPLETE")  // 事件名称:客户端监听"COMPLETE"事件
  .data("Task completed successfully!"));
  } catch (IOException e) {
  emitters.remove(emitter);
  }
  }
  }
  }
  } catch (InterruptedException e) {
  // 任务被中断时,恢复线程中断状态并退出
  Thread.currentThread().interrupt();
  }
  });
  return "Task started!"; // 告诉调用者任务已启动
  }
  }

3.2推荐实现

接下来是一个个人比较推荐的方式,就是设计MessageEventType、MessageEvent、SseEmitterManager。先是消息的事件类型写个枚举类;然后封装一个消息类型的对象,包含数据和类型;最后是一个工具类,生成各种方法供serviceImpl和controller层调用。

MessageEventType
import lombok.Getter;
/**
* @author 馒头
*/
@Getter
public enum MessageEventType {
NEW_MESSAGE("new_message"),
MESSAGE_READ("message_read"),
MESSAGE_UPDATE("message_update"),
INITIAL_DATA("initial_data"),
ERROR("error");
private final String value;
MessageEventType(String value) {
this.value = value;
}
public static MessageEventType fromValue(String value) {
for (MessageEventType type : values()) {
if (type.value.equals(value)) {
return type;
}
}
throw new IllegalArgumentException("未知的消息事件类型: " + value);
}
}
MessageEvent
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 类描述:消息事件类
*
* @ClassName MessageEvent
* @Author ward
* @Date 2025-11-03 12:06
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MessageEvent {
private MessageEventType type;
private Object data;
//  添加这些便捷的静态工厂方法
public static MessageEvent newMessage(Object data) {
return new MessageEvent(MessageEventType.NEW_MESSAGE, data);
}
public static MessageEvent messageRead(Object data) {
return new MessageEvent(MessageEventType.MESSAGE_READ, data);
}
public static MessageEvent initialData(Object data) {
return new MessageEvent(MessageEventType.INITIAL_DATA, data);
}
public static MessageEvent messageUpdate(Object data) {
return new MessageEvent(MessageEventType.MESSAGE_UPDATE, data);
}
public static MessageEvent error(Object data) {
return new MessageEvent(MessageEventType.ERROR, data);
}
// 业务判断方法
public boolean isNewMessage() {
return MessageEventType.NEW_MESSAGE.equals(type);
}
public boolean isMessageRead() {
return MessageEventType.MESSAGE_READ.equals(type);
}
public boolean isInitialData() {
return MessageEventType.INITIAL_DATA.equals(type);
}
public boolean isMessageUpdate() {
return MessageEventType.MESSAGE_UPDATE.equals(type);
}
public boolean isError() {
return MessageEventType.ERROR.equals(type);
}
}
SseEmitterManager

用Map<String, SseEmitter>来存储,存储的时候通过string打上tag,比如用户id。lastHeartbeat 用来记录每个连接的最后活跃时间

import com.heming.weixin.entity.dto.user.Me;
import com.heming.weixin.service.DbSmartMessageService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* @author 馒头
*/
@Slf4j
@Component
public class SseEmitterManager {
private final Map<String, SseEmitter> emitters = new ConcurrentHashMap<>();
  private final ConcurrentHashMap<String, Long> lastHeartbeat = new ConcurrentHashMap<>();
    public SseEmitterManager() {
    ScheduledExecutorService heartbeatScheduler = Executors.newSingleThreadScheduledExecutor();
    heartbeatScheduler.scheduleAtFixedRate(this::sendHeartbeat, 30, 30, TimeUnit.SECONDS);
    // 添加清理超时连接的任务,每5分钟执行一次
    heartbeatScheduler.scheduleAtFixedRate(this::cleanupTimeoutConnections, 1, 5, TimeUnit.MINUTES);
    }
    /**
    * 创建Sse连接
    *
    * @param user 用户
    * @return org.springframework.web.servlet.mvc.method.annotation.SseEmitter
    * @create 2025-11-05
    */
    public SseEmitter createEmitter(Me user) throws IOException {
    String userId = String.valueOf(user.getId());
    SseEmitter emitter = new SseEmitter(0L);
    emitters.put(userId, emitter);
    emitter.onCompletion(() -> {
    emitters.remove(userId);
    log.info("SSE连接完成: {}", userId);
    });
    emitter.onTimeout(() -> {
    emitters.remove(userId);
    log.info("SSE连接超时: {}", userId);
    });
    emitter.onError((e) -> {
    emitters.remove(userId);
    log.error("SSE连接错误: {}, 错误: {}", userId, e.getMessage());
    });
    //发送初始化数据
    sendInitialData(emitter, user);
    return emitter;
    }
    /**
    * 发送初始化数据逻辑
    *
    * @param emitter sse客户端
    * @param user    对应的后台用户
    * @create 2025-11-05
    */
    private void sendInitialData(SseEmitter emitter, Me user) throws IOException {
    String userId = String.valueOf(user.getId());
    try {
    Object rawData = "自定义的数据,可以是数据库查到的";
    MessageEvent initialData = MessageEvent.initialData(rawData);
    //  修正:传递所有必要的参数
    sendEventViaEmitter(emitter,//发送对象
    initialData,//数据
    initialData.getType().getValue(),//消息类型
    "initial-" + userId,//事件id
    30000L);//重连时间
    log.info("已发送SSE初始数据给用户: [{}],[{}],[{}]", userId, user.getRealName(), user.getTel());
    } catch (Exception e) {
    log.error("发送SSE初始数据失败,用户ID: {}", userId, e);
    MessageEvent errorEvent = MessageEvent.error("Failed to load initial data");
    //  修正:传递所有必要的参数
    sendEventViaEmitter(emitter, errorEvent, errorEvent.getType().getValue(), "error-initial", null);
    }
    }
    /**
    * 发送指定类型的事件
    *
    * @param userId 用户id
    * @param event  消息事件
    * @return boolean
    * @create 2025-11-05
    */
    public boolean sendEvent(String userId, MessageEvent event) {
    if (event == null || event.getType() == null) {
    log.warn("SSE发送失败: 事件数据无效");
    return false;
    }
    String eventName = event.getType().getValue();
    return sendMessage(userId, event, eventName, null);
    }
    /**
    * 发送新消息事件
    *
    * @param userId      用户id
    * @param messageData 消息数据
    * @return boolean
    * @create 2025-11-05
    */
    public boolean sendNewMessage(String userId, Object messageData) {
    MessageEvent event = MessageEvent.newMessage(messageData);
    return sendEvent(userId, event);
    }
    /**
    * 发送SSE消息(核心方法)
    *
    * @param userId    用户id
    * @param data      数据
    * @param eventName 事件类型
    * @param retry     重连时间
    * @return boolean
    * @create 2025-11-05
    */
    public boolean sendMessage(String userId, Object data, String eventName, Long retry) {
    if (!validateParameters(userId, data, eventName)) {
    return false;
    }
    SseEmitter emitter = emitters.get(userId);
    if (emitter == null) {
    log.debug("用户SSE连接不存在, userId: {}", userId);
    return false;
    }
    try {
    sendEventViaEmitter(emitter, data, eventName, UUID.randomUUID().toString(), retry);
    updateHeartbeat(userId);
    log.debug("SSE消息发送成功, userId: {}, event: {}", userId, eventName);
    return true;
    } catch (IOException e) {
    handleSendFailure(userId, e);
    return false;
    } catch (Exception e) {
    log.error("SSE消息发送异常, userId: {}", userId, e);
    return false;
    }
    }
    /**
    * 私有发送消息辅助方法(基于SseEmitter.send)
    *
    * @param emitter       客户端
    * @param data          数据
    * @param eventName     事件类型
    * @param eventId       事件id
    * @param reconnectTime 重连时间
    * @create 2025-11-05
    */
    private void sendEventViaEmitter(SseEmitter emitter, Object data, String eventName,
    String eventId, Long reconnectTime) throws IOException {
    SseEmitter.SseEventBuilder eventBuilder = SseEmitter.event()
    .data(data, MediaType.APPLICATION_JSON)
    .name(eventName)
    .id(eventId);
    if (reconnectTime != null) {
    eventBuilder.reconnectTime(reconnectTime);
    }
    emitter.send(eventBuilder);
    }
    /**
    * 校验参数
    *
    * @param userId    用户id
    * @param data      数据
    * @param eventName 事件名字
    * @return boolean
    * @create 2025-11-05
    */
    private boolean validateParameters(String userId, Object data, String eventName) {
    if (StringUtils.isBlank(userId)) {
    log.warn("SSE发送失败: 用户ID为空");
    return false;
    }
    if (data == null) {
    log.warn("SSE发送失败: 消息数据为空, userId: {}", userId);
    return false;
    }
    if (StringUtils.isBlank(eventName)) {
    log.warn("SSE发送失败: 事件名称为空, userId: {}", userId);
    return false;
    }
    return true;
    }
    /**
    * 更新用户心跳时间戳
    *
    * @param userId 用户id
    * @create 2025-11-05
    */
    private void updateHeartbeat(String userId) {
    lastHeartbeat.put(userId, System.currentTimeMillis());
    }
    /**
    * 处理sse发送失败的方法
    *
    * @param userId 用户id
    * @param e      异常信息
    * @create 2025-11-05
    */
    private void handleSendFailure(String userId, IOException e) {
    log.warn("SSE消息发送失败, 移除用户连接, userId: {}, error: {}", userId, e.getMessage());
    removeEmitter(userId);
    }
    /**
    * 主动移除某个用户的连接
    */
    public void removeEmitter(String userId) {
    SseEmitter emitter = emitters.remove(userId);
    lastHeartbeat.remove(userId);
    if (emitter != null) {
    try {
    emitter.complete();
    } catch (Exception e) {
    log.debug("完成emitter时发生异常, userId: {}", userId, e);
    }
    }
    log.info("SSE连接已移除, userId: {}", userId);
    }
    /**
    * 心跳检测
    */
    private void sendHeartbeat() {
    emitters.forEach((userId, emitter) -> {
    try {
    emitter.send(SseEmitter.event()
    .comment("heartbeat")
    .id(String.valueOf(System.currentTimeMillis())));
    } catch (IOException e) {
    emitters.remove(userId);
    log.debug("心跳发送失败,移除用户: {}", userId);
    }
    });
    }
    /**
    * 清理超时连接
    *
    * @create 2025-11-05
    */
    private void cleanupTimeoutConnections() {
    long currentTime = System.currentTimeMillis();
    long timeout = 5 * 60 * 1000; // 5分钟超时
    // 遍历lastHeartbeat,检查哪些连接已经超时
    lastHeartbeat.entrySet().removeIf(entry -> {
    String userId = entry.getKey();
    Long lastBeat = entry.getValue();
    if (lastBeat == null || currentTime - lastBeat > timeout) {
    // 超时,移除连接
    SseEmitter emitter = emitters.get(userId);
    if (emitter != null) {
    emitter.completeWithError(new IOException("Connection timeout"));
    emitters.remove(userId);
    log.info("清理超时连接: {}", userId);
    }
    return true; // 从lastHeartbeat中移除
    }
    return false;
    });
    }

4.客户端实现

我这边是一个消息列表,主要就是服务端发送推文时,客户端能收到消息,并且这个界面分为已读和未读消息,然后还有个阅读全部消息。我的代码给大家参考下

hook.js
import {useHistory} from "react-router";
import request from "../../service/request";
const useMethod = () => {
const history = useHistory();
const {orgCode} = '组织代码';
// 统一的SSE消息处理函数
const handleSSEMessage = (event, messageHandlers = {}) => {
console.log(' 收到SSE消息:', event.data);
try {
const message = JSON.parse(event.data);
console.log(' 原始消息结构:', message);
//  简化:假设所有消息都是 MessageEvent 格式
if (message.type && message.data !== undefined) {
const eventType = message.type;
const eventData = message.data;
console.log(` 处理 ${eventType} 事件:`, eventData);
const handler = messageHandlers[eventType];
if (handler) {
console.log(`✅ 找到 ${eventType} 处理器`);
handler(eventData);
} else {
console.warn('❓ 未处理的事件类型:', eventType);
}
} else {
console.warn('❓ 未知的消息格式:', message);
}
} catch (error) {
console.error('❌ 解析SSE消息失败:', error);
}
};
// 创建SSE连接
const createSSEConnection = (url, messageHandlers, setLoading) => {
console.log(' 开始建立SSE连接...');
const eventSource = new EventSource(url);
// 统一管理连接状态
eventSource.onopen = () => {
console.log('✅ SSE连接已建立');
setLoading?.(false);
};
eventSource.onmessage = (event) => handleSSEMessage(event, messageHandlers);
// 只使用特定事件监听器,因为后端发送的都是有事件名称的消息
Object.keys(messageHandlers).forEach(eventType => {
eventSource.addEventListener(eventType, (event) => {
console.log(` ${eventType} 事件监听器触发:`, event.data);
try {
const data = JSON.parse(event.data);
messageHandlers[eventType](data);
} catch (error) {
console.error(`解析${eventType}失败:`, error);
}
});
});
eventSource.onerror = (error) => {
console.error('❌ SSE连接错误:', error);
setLoading?.(false);
};
return eventSource;
};
const toDetail = async (resourceType, resourceUuid, uuid) => {
try {
// 发送请求表示消息已读
const res = await request.get('/api/message/readOneMessage?uuid=' + uuid);
if (res === true) {
console.log('消息标记为已读:', uuid);
// 根据资源类型跳转
if ("资讯" === resourceType) {
history.push('/news-detail/' + resourceUuid + '?orgCode=' + orgCode);
return;
}
if ("活动" === resourceType) {
const route = await request.get('/api/activity/getActivityRoute?uuid=' + resourceUuid);
history.push(route + '?orgCode=' + orgCode);
}
} else {
console.warn('消息标记为已读失败:', uuid);
// 即使标记已读失败,仍然允许跳转
await handleNavigation(resourceType, resourceUuid);
}
} catch (error) {
console.error('处理消息点击时出错:', error);
// 即使出现错误,也允许用户跳转查看详情
await handleNavigation(resourceType, resourceUuid);
}
}
// 提取导航逻辑到单独函数
const handleNavigation = async (resourceType, resourceUuid) => {
if ("资讯" === resourceType) {
history.push('/news-detail/' + resourceUuid + '?orgCode=' + orgCode);
return;
}
if ("活动" === resourceType) {
try {
const route = await request.get('/api/activity/getActivityRoute?uuid=' + resourceUuid);
history.push(route + '?orgCode=' + orgCode);
} catch (error) {
console.error('获取活动路由失败:', error);
// 提供一个默认路由或错误页面
history.push('/activity-detail/' + resourceUuid + '?orgCode=' + orgCode);
}
}
}
const readAll = async () => {
try {
const res = await request.get('/api/message/readAllMessage');
if (res === true) {
console.log("读取所有消息请求发送成功");
// 注意:现在不再需要在这里更新本地状态
// SSE 会推送更新,触发状态更新
} else {
console.warn("读取所有消息失败:", res);
}
} catch (error) {
console.error("读取所有消息时发生错误:", error);
}
}
return {
toDetail,
readAll,
handleNavigation,
handleSSEMessage,      // 导出统一的SSE消息处理器
createSSEConnection    // 导出创建SSE连接的方法
}
}
export default useMethod;
index.js
import React from "react";
import {Button, CapsuleTabs, Footer, Image, List, Skeleton} from "antd-mobile";
import {useMount, useSetState, useUnmount} from "ahooks";
import {downloadServiceUrl} from "../../service/request";
import useMethod from "./hooks";
const Message = () => {
const [state, setState] = useSetState({
readMessages: [],
unReadMessages: [],
loading: false,
eventSource: null
})
const {
toDetail,
readAll,
createSSEConnection,
} = useMethod();
// 初始化SSE连接
const initSSE = () => {
setState({loading: true});
const messageHandlers = {
initial_data: handleMessageData,
new_message: handleNewMessage,
message_read: handleMessageRead
};
// 一行代码创建连接,自动处理状态
const eventSource = createSSEConnection(
'/api/message/sse',
messageHandlers,
(loading) => setState({loading})
);
setState({eventSource});
};
// 处理初始消息数据
const handleMessageData = (rawData) => {
console.log(' 处理消息数据:', rawData);
// 直接提取消息数组
const unReadMessages = Array.isArray(rawData.data.unReadMessages) ? rawData.data.unReadMessages : [];
const readMessages = Array.isArray(rawData.data.readMessages) ? rawData.data.readMessages : [];
console.log(` 消息统计: ${unReadMessages.length} 条未读, ${readMessages.length} 条已读`);
// 更新状态
setState({
readMessages: readMessages,
unReadMessages: unReadMessages
});
console.log(' 状态更新完成');
};
// 处理新消息
const handleNewMessage = (eventData) => {
console.log(' 收到新消息事件:', eventData);
// 从事件数据中提取实际的消息对象
const newMessage = eventData.data;
if (newMessage && newMessage.uuid) {
console.log(' 添加新消息到未读列表:', newMessage.title);
setState(prevState => ({
unReadMessages: [newMessage, ...prevState.unReadMessages]
}));
} else {
console.warn('⚠️ 新消息数据格式异常:', eventData);
}
};
// 处理消息已读状态更新
const handleMessageRead = (readData) => {
if (readData.messageId) {
setState(prevState => {
const readMessageIndex = prevState.unReadMessages.findIndex(
msg => msg.uuid === readData.messageId
);
if (readMessageIndex !== -1) {
const readMessage = prevState.unReadMessages[readMessageIndex];
const newUnReadMessages = [...prevState.unReadMessages];
newUnReadMessages.splice(readMessageIndex, 1);
return {
unReadMessages: newUnReadMessages,
readMessages: [readMessage, ...prevState.readMessages]
};
}
return prevState;
});
}
};
// 批量阅读所有消息
const handleReadAll = async () => {
try {
await readAll();
// 阅读全部后,前端立即更新状态
setState(prevState => ({
readMessages: [...prevState.unReadMessages, ...prevState.readMessages],
unReadMessages: []
}));
} catch (error) {
console.error('阅读全部消息失败:', error);
}
};
useMount(() => {
initSSE();
});
useUnmount(() => {
// 组件卸载时关闭SSE连接
if (state.eventSource) {
state.eventSource.close();
}
});
if (state.loading) {
return <Skeleton/>;
  }
  return (
  <List header='我的消息'>
    {(Array.isArray(state.unReadMessages) && state.unReadMessages.length > 0) && (
    <Button block color='success' size='middle' onClick={handleReadAll}>
      阅读全部消息
      </Button>
        )}
        <CapsuleTabs>
          <CapsuleTabs.Tab title='未读消息' key='unReadMessage'>
            {state.unReadMessages.map(user => (
            <List.Item
            onClick={() => toDetail(user.type, user.resourceUuid, user.uuid)}
            key={user.uuid}
            prefix={
            <Image
            src={`${downloadServiceUrl}?fileId=${user.img}`}
            style={{borderRadius: 20}}
            fit='cover'
            width={40}
            height={40}
            />
            }
            description={user.messageDescribe}
            >
            {user.title}
            </List.Item>
              ))}
              </CapsuleTabs.Tab>
                <CapsuleTabs.Tab title='已读消息' key='readMessage'>
                  {state.readMessages.map(user => (
                  <List.Item
                  onClick={() => toDetail(user.type, user.resourceUuid, user.uuid)}
                  key={user.uuid}
                  prefix={
                  <Image
                  src={`${downloadServiceUrl}?fileId=${user.img}`}
                  style={{borderRadius: 20}}
                  fit='cover'
                  width={40}
                  height={40}
                  />
                  }
                  description={user.messageDescribe}
                  >
                  {user.title}
                  </List.Item>
                    ))}
                    </CapsuleTabs.Tab>
                      </CapsuleTabs>
                        <Footer label='没有更多了'/>
                          </List>
                            );
                            };
                            export default Message;

踩坑

1、一定要注意后端发送的事件类型,和客户端监听的要保持一致,也就是下面两幅图的位置要一致,要不然客户端收不到消息。在这里插入图片描述
在这里插入图片描述