云计算百科
云计算领域专业知识百科平台

【SSE实战】:拒绝轮询!一套代码吃透 SSE!(用「fetchEventSource」一把梭搞定 SSE 实时推流)

目录

第一章 前言

第二章 准备工作

2.1 SSE 协议到底规定了啥

2.2 SSE 专属的“直播监控台”

2.3 fetchEventSource库

2.4 原生 EventSource

第三章 实战

3.1 封装hooks,建立连接

3.2 使用

3.3 事件分发

3.4 异常与重连

3.5 页面卸载时清理

3.6 安全与性能

第四章 总结


第一章 前言

        最近遇到一个前人写的需求很蒙圈;为了实现一个状态的实时变化:如后台有数据的状态更新了需要前端刷新数据,于是前端就用setInterval写了一个每隔5s刷新一下数据列表,如果用户还进行了某些操作,再缓存一份,当数据刷新了之后再还原回来,给客户一种无感的感觉;当看到这块代码就很懵圈呀!就为了一个那样的需求,就用 setInterval 刷新数据?太掉价了!导致页面基本上都在向服务器请求数据,服务器压力大,前端也有一点超级无敌卡!秉承着凑合着用是吧!

        小编为了处理上面那需求,为了优化这块内容,就跟后端商量好,决定使用SSE处理这个功能!下面是小编处理的相关基本代码(注意!不是具体功能的哟!)

第二章 准备工作

针对SSE,小编要带大家下认识几个工具,方便之后对接时的调试……

2.1 SSE 协议到底规定了啥

  • HTTP 长连接

响应头必须带:

Content-Type: text/event-stream

请求头必须有:

Accept: text/event-stream

  • 之后服务器保持 TCP 不断,按格式吐文本:

event: message
data: {"id":1,"amount":99}
id:1001
retry: 5000

  • 浏览器原生能力
  • 遇到 \\n\\n 触发一次事件
  • 断网后自动重连,并带上 Last-Event-ID
  • 可调整重连间隔 retry:(默认 3s → 指数退避)
  • 2.2 SSE 专属的“直播监控台”

    • EventStream 就是 Chrome DevTools → Network 面板 里专门用来可视化 SSE(text/event-stream)长连接的调试栏。
    • 只要响应头包含 Content-Type: text/event-stream Chrome 就会把这条请求自动归类到 EventStream 标签页(而不是常规的 Response/Preview)。
    • 在里面可以: 实时看到服务器一条条 push 过来的事件(data: / event: / id:); 检查断线重连是否带 Last-Event-ID; 过滤心跳、统计延迟、复制原始报文。

    2.3 fetchEventSource库

    前端问:为什么很多时候代码里看不到携带了text/event-stream,但是当我们看请求头的时候又会有呢?如下代码:

    import { fetchEventSource } from '@microsoft/fetch-event-source'

    await fetchEventSource('/api/***/****', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ userId: 123 }),
    onmessage(msg) { console.log(msg.data) }
    })

    回答:是这个库在背后偷偷干了三件事:

  • 自动追加请求头 Accept: text/event-stream
  • 校验响应头必须是 Content-Type: text/event-stream,否则抛错
  • 按 SSE 格式逐段解析并回调 onmessage / onerror / onclose
  • 因此:「前端用了这个库之后代码层面」看不到 event-stream,「协议层面」仍在严格遵守 SSE 规范。 这也解释了为何 Chrome 依然把它收进 EventStream 栏。

    2.4 原生 EventSource

    不使用原生EventSource的原因,三大痛点:

  • 只能 GET,不能带 body;
  • 不能自定义头(如 Authorization);
  • 不能主动 cancel。
  • 第三章 实战

    3.1 封装hooks,建立连接

    /**
    * SSE (Server-Sent Events) Hook
    * 用于在 Vue 组件中管理 SSE 连接
    */
    import { ref } from 'vue'
    import { fetchEventSource } from '@microsoft/fetch-event-source'
    import _c from '@/config/index'

    /**
    * SSE 连接管理 Hook
    * @returns {Object} 包含控制器和 SSE 操作方法的对象
    */
    export function useSse() {
    // 用于管理 SSE 连接的控制器实例
    const controller = ref(null)

    /**
    * 启动 SSE 连接
    * @param {Object} data – 发送给服务器的请求数据
    * @param {Function} callBack – 接收 SSE 消息的回调函数
    */
    async function startSSE(data, callBack, url) {
    // 创建新的 AbortController 实例
    controller.value = new AbortController()

    // 建立 SSE 连接
    fetchEventSource(_c.baseApiPrefix + url, {
    method: 'POST',
    headers: {
    'Content-Type': 'application/json'
    },
    // 请求体数据,转换为 JSON 字符串
    body: JSON.stringify(data),
    // 用于终止连接的信号
    signal: controller.value.signal,
    // 禁止后台重连
    openWhenHidden: false,
    // 连接建立时的回调
    onopen() {
    console.log('建立连接的回调')
    },
    // 连接关闭时的回调
    onclose() {
    console.log('关闭连接的回调')
    // 关闭 SSE 连接
    closeSSE()
    },
    // 接收消息时的回调
    onmessage(msg) {
    console.log('收到 SSE 消息:', msg)
    // 解析 JSON 数据并传递给回调函数
    callBack(JSON.parse(msg.data))
    },
    // 连接错误时的回调
    onerror(err) {
    // 通知回调函数发生错误
    callBack({ event: 'error', data: err })
    // 关闭 SSE 连接
    closeSSE()
    // 抛出错误
    throw err
    }
    })
    }

    /**
    * 关闭 SSE 连接
    */
    function closeSSE() {
    if (controller.value) {
    // 终止当前连接
    controller.value.abort()
    // 清空控制器引用
    controller.value = null
    }
    }

    // 返回控制器和操作方法
    return {
    controller,
    startSSE,
    closeSSE
    }
    }

    解释:

    controller = new AbortController();
    await fetchEventSource(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
    signal: controller.signal,
    openWhenHidden: false // 切后台就暂停,省流量
    })

    • 用 AbortController 实现“可取消的 Promise”;
    • 把业务参数(查询条件、用户 ID 等)放在 body,避免 URL 过长。

    3.2 使用

    import { useSse } from '@/hooks/useSse'
    const { startSSE } = useSse()

    const chatList = ref([])

    // 前端需要模拟问答时两种方式的对象
    //问
    chatList.value.push({
    mathId: new Date().getTime() + Math.random(),
    type: 'ask',
    text: val,
    files: files
    })
    //答
    chatList.value.push({
    mathId: new Date().getTime() + Math.random(),
    type: 'answer',
    answer: '',
    thoughts: ''
    })

    const setAnswer = (obj) => {
    switch (obj.event) {
    case 'message': {
    // 处理后端返回message时的逻辑
    break
    }
    case 'message_end':
    // 消息结束时的逻辑
    break
    }
    // scrollToBottom()
    }

    3.3 事件分发

    后端可以发三种标准事件:

    event: message → onmessage 默认
    event: ping → 心跳,前端忽略即可
    event: message_end → 本次批次的结束标记

    前端只需要针对事件类型message与message_end处理逻辑即可;如果可以工具类用 handlers[msg.event] 做 map 分发,比 if/else 易扩展。

    const handlers = {
    message: () => {
    try {
    callBack(JSON.parse(msg.data))
    } catch (e) {
    callBack({ event: 'error', data: 'Failed to parse message data' })
    }
    },
    ping: () => {},
    message_end: () => {
    try {
    callBack(JSON.parse(msg.data))
    } catch (e) {
    callBack({ event: 'error', data: 'Failed to parse message_end data' })
    }
    }
    }
    const handler = handlers[msg.event]
    if (handler) {
    handler()
    }

    3.4 异常与重连

    fetchEventSource 内置指数退避重连(默认 3 s → 6 s → 12 s…)。 我们只需在 onerror 里做三件事:

  • 告诉业务层 {event:'error'};
  • closeSSE() 清掉旧连接;
  • 不再 throw,否则控制台会出现 Uncaught Promise。
  • 3.5 页面卸载时清理

    防止切路由后后台仍占用连接数。

    () => closeSSE()

    3.6 安全与性能

    • 鉴权:POST 头里带 Authorization: Bearer <token>,Spring 按常规拦截器校验即可;SSE 连接一旦 401,前端 onerror 收到立刻跳转登录页。

      – 放在请求体中:

    headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + (getToken() || 'test')
    }

    – 放在参数中:

    const data = {
    userId: 123,
    inputs: {
    token: getToken() ? `Bearer ${getToken()}` : 'Bearer test'
    },
    appKey: appKey.value,
    chatId: currentChatId.value,
    ……
    }

    • 多实例(针对后端的):内存 ConcurrentHashMap 只在本机有效。上生产改用 Redis 发布/订阅或 Kafka → 每个实例只推自己内存里的会话。
    • 限流: 单个浏览器会话最高 1 条连接;超出返回 429,前端退避 5 s 再重试。
    • 压缩: 事件体 > 1 kB 可开 gzip(Nginx gzip_types text/event-stream;),但小消息反而增加 CPU,按需开启。

    第四章 总结

    前端 fetchEventSource 自动 Accept: text/event-stream
    ↓ POST + Token
    后端 SseEmitter 响应 Content-Type: text/event-stream
    ↓ 数据变更
    Chrome DevTools → EventStream 栏实时滚动

    • 对于前端:使用fetchEventSource库,协议没变,只是换了更灵活的敲门方式。
    • 再者SSE 的"实时"关键不在前端,而在后端如何感知数据变化。
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【SSE实战】:拒绝轮询!一套代码吃透 SSE!(用「fetchEventSource」一把梭搞定 SSE 实时推流)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!