网络传输协议的介绍——SSE
今天实战的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可能导致客户端解析错误。
: 这是注释(客户端会忽略)\ndata: 这是第1条消息\n\ndata: 这是第2条消息的第一行\ndata: 这是第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,//事件id30000L);//重连时间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.ItemonClick={() => toDetail(user.type, user.resourceUuid, user.uuid)}key={user.uuid} prefix={<Imagesrc={`${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.ItemonClick={() => toDetail(user.type, user.resourceUuid, user.uuid)}key={user.uuid}prefix={<Imagesrc={`${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、一定要注意后端发送的事件类型,和客户端监听的要保持一致,也就是下面两幅图的位置要一致,要不然客户端收不到消息。

