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,解析处理就是问号,导致用户名字中文都变成了问号,后端把数据改成传输二进制数据,而不是传世字符串,就能解决问号的问题

相关推荐
ᥬ 小月亮6 分钟前
Uniapp中自定义导航栏
javascript·css·uni-app
柯南二号7 分钟前
【大前端】React useEffect 详解:从入门到进阶
前端·react.js·前端框架
ftpeak19 分钟前
Rust Web开发指南 第六章(动态网页模板技术-MiniJinja速成教程)
开发语言·前端·后端·rust·web
南囝coding37 分钟前
Claude Code 官方内部团队最佳实践!
前端·后端·程序员
开开心心就好39 分钟前
文档格式转换软件 一键Word转PDF
开发语言·前端·数据库·pdf·c#·word
袁煦丞1 小时前
Redis内存闪电侠:cpolar内网穿透第614个成功挑战
前端·程序员·远程工作
BillKu1 小时前
Vue3组件加载顺序
前端·javascript·vue.js
IT_陈寒1 小时前
Python性能优化必知必会:7个让代码快3倍的底层技巧与实战案例
前端·人工智能·后端
暖木生晖2 小时前
引入资源即针对于不同的屏幕尺寸,调用不同的css文件
前端·css·媒体查询
袁煦丞2 小时前
DS file文件管家远程自由:cpolar内网穿透实验室第492个成功挑战
前端·程序员·远程工作