前端接入sse(EventSource)(@fortaine/fetch-event-source)

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核心库里面的这个方法时候是可以的,下面是实际原因和解决方案
相关推荐
阿星AI工作室2 小时前
gemini3手势互动圣诞树保姆级教程来了!附提示词
前端·人工智能
徐小夕2 小时前
知识库创业复盘:从闭源到开源,这3个教训价值百万
前端·javascript·github
xhxxx2 小时前
函数执行完就销毁?那闭包里的变量凭什么活下来!—— 深入 JS 内存模型
前端·javascript·ecmascript 6
StarkCoder2 小时前
求求你试试 DiffableDataSource!别再手算 indexPath 了(否则迟早崩)
前端
fxshy2 小时前
Cursor 前端Global Cursor Rules
前端·cursor
WindStormrage3 小时前
umi3 → umi4 升级:踩坑与解决方案
前端·react.js·cursor
十一.3663 小时前
103-105 添加删除记录
前端·javascript·html
用户47949283569153 小时前
面试官:DNS 解析过程你能说清吗?DNS 解析全流程深度剖析
前端·后端·面试