uniapp在微信小程序中实现 SSE进行通信

最近帮忙做一个项目的前端,遇到了一种通信方式就是SSE,这种也算是我的薄弱点,以前实现通信往往是轮询和websocket,现在用到了另外一种方法,SSE是服务端向客户端发送消息的方法,只能输出的流式块,而且还是在小程序里面使用,和在网页里面使用是不一样的,网页有专门的方法,可以用new EventSource('http://地址') 来实现

小程序里

项目背景是一个校园小程序,好友把店铺推荐给其他好友,前端实时做出响应

建立SSE-> 好友发送推荐消息-> 前端收到SSE传来的消息-> 调用已经订阅的函数 -> 小程序悬浮窗显示小红点 -> 点击小程序悬浮窗打开好友的推荐通知 -> 再点击一次跳转好友推荐

js 复制代码
//具体实现是订阅发布,先订阅具体的方法,收到消息向对方发布推荐

let requestTask = null;
let reconnectTimer = null;
let status = 'disconnected'; // 'disconnected', 'connecting', 'connected'
let listeners = new Set();
let restart = true;

const SSE_URL = 'https://localhost:8080/user/sse/subscribe'
// 广播消息给所有监听者
function broadcast(data) {
	listeners.forEach(listener => {
		try {
			listener(data);
		} catch (e) {
			console.error("SSE listener 执行出错:", e);
		}
	});
}

function handleChunk(chunk) {
	broadcast(chunk);
}

function connect() {
	if (requestTask || status === 'connecting') {
		return;
	}
	console.log("SSE: 正在连接...");
	status = 'connecting';
	requestTask = uni.request({
		url: SSE_URL,
		method: 'GET',
		enableChunked: true,
		header: {
			Accept: 'text/event-stream',
			'authentication': getApp().globalData.token,
		},
		responseType: 'arraybuffer',
		fail: error => {
			console.log("error:" + JSON.stringify(error))
			if (restart && !reconnectTimer) {
				reconnectTimer = setTimeout(() => {
					connect();
				}, 3000);
			}
		},
		complete: res => {
			requestTask = null;
			status = 'disconnected';

			if (restart && !reconnectTimer) {
				reconnectTimer = setTimeout(() => {
					connect();
				}, 3000);
			}
		}
                //一定要写ssuccss,fail,complete这个三个里面的任意一个.不然requestTask是一个promise,导致requestTask.onChunkReceived出错,ssuccss这些在链接建立的时候不会调用,在调试器可以看到pedding的状态,这是正常的,我在这边主要是写了重连的方法
	});
	console.log(requestTask)
	const decoder = new TextDecoder('utf-8');
	let buffer = '';
        
        
        //所有后端发来的消息都会调用这个
	requestTask.onChunkReceived((res) => {
		console.log(res.data)
		
		
		// res.data 是一个 ArrayBuffer,我们先转成 Uint8Array
		const uint8Array = new Uint8Array(res.data);
		// 使用 TextDecoder 将 Uint8Array 解码成字符串
		const chunkText = decoder.decode(uint8Array, {
			stream: true
		});

		// 将新收到的文本块追加到缓冲区
		buffer += chunkText;
		// SSE 消息以两个换行符 `\n\n` 分隔
		const messages = buffer.split('\n\n');
		// 最后一个元素可能是不完整的消息,把它放回缓冲区,等待下一个数据块
		buffer = messages.pop();
		// 遍历所有完整的消息进行处理
		messages.forEach(message => {
			if (!message) return; 
			console.log("收到的完整SSE消息:", message);
			const sseMessage = {
				event: 'message',
				data: null,
			};
			// 逐行解析消息
			const lines = message.split('\n');
			for (const line of lines) {
				if (line.startsWith('event:')) {
					sseMessage.event = line.substring(6).trim();
				} else if (line.startsWith('data:')) {
					const dataPart = line.substring(5).trim();
					if (sseMessage.data === null) {
						sseMessage.data = dataPart;
					} else {
						sseMessage.data += '\n' + dataPart;
					}
				}
			}

			if (sseMessage.data) {
				try {
					sseMessage.data = JSON.parse(sseMessage.data);
				} catch (e) {
					console.error("解析 data 字段的 JSON 失败:", e, "原始data:", sseMessage.data);
				}
			}

			console.log("最终解析结果:", sseMessage);
			handleChunk(sseMessage);
			if (sseMessage.event === "friend_recommend") {
				console.log("进入emit", sseMessage.data);
				uni.$emit('show-recommendation', sseMessage.data);
			}
		});
	});

}

//这边都是暴露出去使用的方法,用于订阅什么的

/**
 * @description 启动 SSE 连接(如果未启动)
 */
export function startSSE() {
	if (!requestTask) {
		connect();
	}
}

/**
 * @description 停止 SSE 连接
 */
export function stopSSE() {
	console.log("SSE: 停止连接");
	restart = false;

	// 清除重连定时器
	if (reconnectTimer) {
		clearTimeout(reconnectTimer);
		reconnectTimer = null;
	}

	// 中止请求
	if (requestTask) {
		requestTask.abort();
		requestTask = null;
	}
	status = 'disconnected'
}

/**
 * @description 订阅消息
 * @param {Function} callback - 页面用于接收消息的回调函数
 */
export function subscribe(callback) {
	if (typeof callback !== 'function') {
		console.error("SSE: 订阅失败,回调必须是函数");
		return;
	}

	if (!listeners.has(callback)) {
		listeners.add(callback);
		console.log('SSE: 添加新的监听器,当前数量:', listeners.size);

		if (!requestTask || status === 'disconnected') {
			startSSE();
		}
	}
}

/**
 * @description 取消订阅
 * @param {Function} callback - 页面之前传入的回调函数
 */
export function unsubscribe(callback) {
	if (listeners.has(callback)) {
		listeners.delete(callback);
		console.log('SSE: 移除监听器,当前数量:', listeners.size);

		// 如果没有监听器,则停止连接
		if (listeners.size === 0) {
			stopSSE();
		}
	}
}

/**
 * @description 获取当前连接状态
 */
export function getStatus() {
	return status;
}

/**
 * @description 获取当前监听器数量
 */
export function getListenerCount() {
	return listeners.size;
}

/**
 * @description 清除所有监听器
 */
export function clearAllListeners() {
	console.log('SSE: 清除所有监听器');
	listeners.clear();
	stopSSE();//
}

值得注意的是,在const uint8Array = new Uint8Array(res.data);解析后端数据的时候,遇到了中文在传输时候ArrayBuffer里面的中文的编码都变成了63,解析处理就是问号,导致用户名字中文都变成了问号,后端把数据改成传输二进制数据,而不是传世字符串,就能解决问号的问题

相关推荐
程序员Agions13 小时前
程序员邪修手册:那些不能写进文档的骚操作
前端·后端·代码规范
jqq66613 小时前
解析ElementPlus打包源码(五、copyFiles)
前端·javascript·vue.js
Awu122714 小时前
⚡IndexedDB:现代Web应用的高性能本地数据库解决方案
前端·indexeddb
似水流年_zyh14 小时前
canvas写一个选择音频区域的组件
前端·canvas
wordbaby14 小时前
TanStack Router 实战:如何优雅地实现后台管理系统的“多页签” (TabList) 功能
前端·react.js
凌览14 小时前
2026年1月编程语言排行榜|C#拿下年度语言,Python稳居第一
前端·后端·程序员
user861581857815414 小时前
Element UI 表格 show-overflow-tooltip 长文本导致闪烁的根本原因与解法
前端
不会写前端的小丁14 小时前
前端首屏渲染性能优化小技巧
前端
还不秃顶的计科生14 小时前
defaultdict讲解
开发语言·javascript·ecmascript
晴虹14 小时前
lecen:一个更好的开源可视化系统搭建项目--组件和功能按钮的权限控制--全低代码|所见即所得|利用可视化设计器构建你的应用系统-做一
前端·后端·低代码