今天实战的SSE协议,这个协议是基于HTTP的一个轻量级单向传输协议,允许服务器主动向客户端推送实时数据,场景主要有:新闻推送、消息通知、股票行情、实时日志等。
核心特性如下:
1、单向通信
2、基于HTTP
3、长连接(替代轮询)
4、自动重连
1.客户端基本使用方法
这里简单画了个流程图表示生命周期。
1、先是连接成功触发open事件;
2、然后接收message消息是要配置监听的,建议用addEventListener,因为如果使用onMessage无法接收指定消息类型,主要就是后面服务端推送的时候会指定类型。前端接收的都是字符串类型,注意后端如果是json格式要用JSON.parse进行转换;
3、网络中断,服务器出错都会触发error事件。
4、关闭连接的close方法,通常离开页面就要关闭

javascript
//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可能导致客户端解析错误。
css
: 这是注释(客户端会忽略)\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列表,只要有客户端连接就存入列表,所有连接的客户端都存在这里。这样存在的问题是不能定向推送,场景有局限。
java
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
java
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
java
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 用来记录每个连接的最后活跃时间
java
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
javascript
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
javascript
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、一定要注意后端发送的事件类型,和客户端监听的要保持一致,也就是下面两幅图的位置要一致,要不然客户端收不到消息。
