网络传输协议的介绍——SSE

今天实战的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、一定要注意后端发送的事件类型,和客户端监听的要保持一致,也就是下面两幅图的位置要一致,要不然客户端收不到消息。

相关推荐
quant_19862 小时前
【教程】使用加密货币行情接口 - 查询比特币实时价格
开发语言·后端·python·websocket·网络协议
QT 小鲜肉2 小时前
【QT/C++】Qt网络编程进阶:UDP通信和HTTP请求的基本原理和实际应用(超详细)
c语言·网络·c++·笔记·qt·http·udp
闲人编程3 小时前
用Python和Telegram API构建一个消息机器人
网络·python·机器人·api·毕设·telegram·codecapsule
掘根3 小时前
【Docker】网络
网络·docker·容器
Grass Router 小草聚合路由4 小时前
GrassRouter融合通信设备-多链路聚合路由在各行业的应急网络中的重要作用和解决方案
网络·多链路聚合·应急保障设备·多链路聚合通信设备·聚合路由·多卡聚合通信设备·5g聚合路由设备
我就是一粒沙5 小时前
网络安全培训
网络·安全·web安全
Jerry2505095 小时前
怎么才能实现网站HTTPS访问?
网络协议·http·网络安全·https·ssl
tang777896 小时前
对抗高级反爬:基于动态代理 IP 的浏览器指纹模拟与轮换策略
网络·网络协议·tcp/ip
oil欧哟6 小时前
Agent 设计与上下文工程- 02 Workflow 设计模式(上)
前端·网络·人工智能