1、使用原生的原生 EventSource
特性:1、自动重连(固定3秒重试) 2、无法手动设置header头信息,只能使用get请求
typescript
// SSE 连接
const sse: InfoStore['sse'] = (
onMessage?: (data: any) => void,
onError?: (error: any) => void,
onOpen?: () => void,
onClose?: () => void
) => {
// 从 LocalStorage 获取认证令牌
const token = 你的token
// 如果没有令牌,提示错误并返回
if (!token) {
message.error('未获取到认证令牌,无法建立 SSE 连接');
if (onError) {
onError(new Error('未获取到认证令牌'));
}
return;
}
// 客户端 ID
// 构建完整的请求 URL,包含查询参数
const url = new URL('/api/resource/sse', window.location.origin);
url.searchParams.append('tokenValue', `${token}`);
const en = new EventSource(url)
en.onmessage = event => {
console.log('e.data', event.data)
let data: any = event.data;
try {
data = JSON.parse(event.data);
console.log('event--消息接收成功', data)
} catch {
// 如果不是 JSON 格式,直接使用原始数据
}
}
en.onerror = e => {
console.log('err', e)
}
en.onopen = e => {
console.log('onopen', e)
}
return
}
fetch + ReadableStream 手动实现
特点:可操作性更加高一点
typescript
const sse: DocumentLibInfoStore['sse'] = (
onMessage?: (data: any) => void,
onError?: (error: any) => void,
onOpen?: () => void,
onClose?: () => void
) => {
const token = LocalStorage.getToken();
if (!token) {
message.error('未获取到认证令牌,无法建立 SSE 连接');
onError?.(new Error('未获取到认证令牌'));
return () => {};
}
const clientid = 'e5cd7e4891bf95d1d19206ce24a7b32e';
// 重连配置(可根据业务调整)
let reconnectCount = 0; // 当前重连次数
const maxReconnectTimes = 10; // 最大重连次数(0 表示无限重连)
let baseReconnectInterval = 3000; // 初始重连间隔(3秒,和原生EventSource一致)
let abortController: AbortController | null = null;
let isManualClose = false; // 主动关闭标识(避免主动关了还重连)
// 核心:封装 SSE 连接函数(包含重连逻辑)
const connectSSE = async () => {
// 重置控制器
abortController = new AbortController();
try {
// 1. 前置处理:若重连时 token 过期,可先刷新 token(可选)
const currentToken = LocalStorage.getToken();
if (!currentToken) {
throw new Error('token 已过期,停止重连');
}
// 2. 发起 fetch 请求(支持自定义 Header)
const response = await fetch('/api/resource/sse', {
method: 'GET',
headers: {
'Authorization': `Bearer ${currentToken}`,
'Clientid': clientid,
'Accept': 'text/event-stream',
},
signal: abortController.signal,
credentials: 'include',
});
// 3. 处理 HTTP 错误(区分是否重连)
if (!response.ok) {
const errorMsg = `SSE 连接失败:${response.status} ${response.statusText}`;
// 规则:401/403(鉴权失败)不重连,5xx(服务端临时错误)重连,4xx 其他不重连
if (response.status >= 500 && response.status < 600) {
throw new Error(`服务器临时错误:${errorMsg},将尝试重连`);
} else if (response.status === 401) {
LocalStorage.removeToken(); // 清除过期 token
throw new Error('token 失效,停止重连');
} else {
throw new Error(errorMsg + ',不重连');
}
}
// 4. 连接成功:重置重连计数和间隔
reconnectCount = 0;
baseReconnectInterval = 3000;
onOpen?.();
message.success('SSE 连接成功');
// 5. 读取流式响应(和之前逻辑一致)
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) {
// 流正常关闭:若不是主动关闭,触发重连
if (!isManualClose) {
throw new Error('SSE 流正常关闭,尝试重连');
}
break;
}
// 解析 SSE 消息(原有业务逻辑)
buffer += decoder.decode(value, { stream: true });
const messages = buffer.split('\n\n');
buffer = messages.pop() || '';
for (const msg of messages) {
if (msg.startsWith('data: ')) {
const dataStr = msg.slice(6);
try {
const data = JSON.parse(dataStr);
onMessage?.(data);
// 你的业务逻辑(更新进度、刷新列表等)
const flagProcessingList = JSON.parse(JSON.stringify(state.processingList));
flagProcessingList.forEach((item: any) => {
if (item.documentId === data.documentId) {
item.progress = data.progress;
item.status = data.status;
if (data.progress === 100 || data.message === 'error') {
api.fetchDocumentLibList(0);
api.fetchDocumentLibList(1);
}
}
});
actions.setProcessingList(flagProcessingList);
} catch (e) {
onMessage?.(dataStr);
}
}
}
}
} catch (error) {
const err = error as Error;
// 排除主动关闭的错误(AbortError)
if (err.name === 'AbortError' && isManualClose) {
console.log('SSE 主动关闭,不重连');
onClose?.();
return;
}
// 触发错误回调
onError?.(err);
message.error(`SSE 连接异常:${err.message}`);
// 重连逻辑:未达最大次数 + 不是主动关闭 + 不是鉴权失败
if (!isManualClose && reconnectCount < maxReconnectTimes) {
reconnectCount++;
// 指数退避:重连间隔翻倍(3s→6s→12s...),上限 30s
const currentInterval = Math.min(baseReconnectInterval * Math.pow(2, reconnectCount - 1), 30000);
console.log(`SSE 将在 ${currentInterval/1000} 秒后重连(第 ${reconnectCount} 次)`);
// 延迟重连
setTimeout(() => {
connectSSE();
}, currentInterval);
} else if (reconnectCount >= maxReconnectTimes) {
message.error('SSE 重连次数已达上限,停止重连');
onClose?.();
}
}
};
// 启动首次连接
connectSSE();
// 返回手动关闭函数(标记主动关闭,终止重连)
return () => {
isManualClose = true; // 标记为主动关闭
if (abortController) {
abortController.abort(); // 取消 fetch 请求
}
onClose?.();
console.log('SSE 已手动关闭,终止重连');
};
};
使用 @fortaine/fetch-event-source
这个插件的使用就比较简单也比较方便可以参考官方API地址
@fortaine/fetch-event-source和使用原生的原生 EventSource对比如下图

- 当我实际使用的时候遇到了一个问题: @fortaine/fetch-event-source 调用的这个库sse方法的时候,第二次建立的时候会把直接走onclose方法关闭掉的同时我就接收不到消息了,而使用 const useEventSource: typeof import('@vueuse/core')['useEventSource'] vue3核心库里面的这个方法时候是可以的,下面是实际原因和解决方案







