最近帮忙做一个项目的前端,遇到了一种通信方式就是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,解析处理就是问号,导致用户名字中文都变成了问号,后端把数据改成传输二进制数据,而不是传世字符串,就能解决问号的问题