前端实现 Server-Sent Events 全解析:从代码到调试的实战指南

什么是 Server-Sent Events

当你在使用 ChatGPT 等 AI 对话产品时,是否注意到回答内容会逐字出现在屏幕上?这种"打字机"效果背后,很可能就是 Server-Sent Events(SSE)技术在发挥作用。与传统的 AJAX 请求不同,SSE 允许服务器在建立一次连接后持续向客户端推送数据,特别适合需要实时更新的场景。

SSE 本质上是一种基于 HTTP 的 server push 技术,它通过特殊的 text/event-stream 响应类型,让服务器能够随时向客户端发送数据。与 WebSocket 相比,SSE 具有实现简单轻量级自动重连等优势,非常适合单向的实时数据推送场景。

SSE 前端实现核心代码解析

API 封装层设计

我们先来看最上层的 API 封装。下面这段代码定义了一个 questionAPI 函数,它是与 SSE 服务端交互的入口:

js 复制代码
// 智能问答 API 封装
export const questionAPI = (data, { onMessage, onComplete }) => {
  return request({
    method: "post",
    url: "/chats",
    data: data,
    isSSE: true,
    onMessage,  // 传递消息回调
    onComplete  // 传递完成回调
  });
};

这个封装有几个关键点:

  • 通过 isSSE: true 标记这是一个 SSE 请求
  • 接收 onMessage 和 onComplete 两个回调函数
  • 与普通 API 调用方式保持一致,降低使用门槛

请求层实现

接下来是底层的 request 函数实现,这是处理 SSE 的核心部分:

该部分封装用的是uni.request,这部分根据你的实际开发环境来替换。逻辑是不变得

js 复制代码
return new Promise((resolve, reject) => {
  if (options.isSSE) {
    let buffer = "";  // 缓存未完整的数据块
    const requestTask = uni.request({
        url: "xxx" + options.url,
      method: options.method,
      data: options.data || options.params,
      header: {
        "Content-Type": "application/json",
        "Accept": "text/event-stream",  // SSE 必需请求头
        ...(token && { token }),
      },
      enableChunked: true,  // 开启分块传输
      responseType: "stream",  // 流类型响应
      success: (res) => {
        console.log("SSE 请求完成", res);
      },
      fail: (err) => {
        console.error("SSE 请求失败", err);
        uni.hideLoading();
        reject(err);
      },
      complete: () => {
        console.log("SSE 请求结束");
        uni.hideLoading();
        // 处理缓冲区剩余数据
        if (buffer.trim()) {
          if (options.onMessage) {
            options.onMessage(buffer);
          }
        }
        if (options.onComplete) {
          options.onComplete();
        }
        resolve();
      },
    });

    // 监听分块数据接收
    requestTask.onChunkReceived((res) => {
      uni.hideLoading();
      try {
        // 将二进制数据解码为文本
        const chunk = new TextDecoder().decode(new Uint8Array(res.data));
        buffer += chunk;  // 追加到缓冲区

        // 按 SSE 格式分割数据(双换行符分隔)
        const messages = buffer.split("\n\n");
        // 保留最后一个可能不完整的消息
        buffer = messages.pop() || "";

        // 处理完整的消息
        messages.forEach((message) => {
          if (message.trim()) {
            // 触发回调函数
            if (options.onMessage) {
              options.onMessage(message);
            }
          }
        });
      } catch (error) {
        console.error("解析 SSE 数据失败:", error);
      }
    });
  } else {
    // 普通请求处理逻辑...
  }
});

这段代码实现了 SSE 的核心功能,主要包括:

  1. 设置正确的请求头:Accept: "text/event-stream" 是 SSE 的标志性请求头
  2. 开启分块传输:enableChunked: true 确保能够接收流式数据
  3. 分块数据处理:通过 onChunkReceived 事件监听数据块到达
  4. 数据缓冲区:使用 buffer 变量处理可能被分割的不完整数据块
  5. SSE 格式解析:按双换行符 \n\n 分割完整的 SSE 消息

业务逻辑层调用

最后是在 Vue 组件中如何使用这个 API:

js 复制代码
questionAPI(
  {
    user: user,
    session_id: questionContent.value.session_id,
    messages: [
      {
        role: "user",
        content: type === "otherQuestions" ? item : sendMessage.value,
      },
    ]
  },
  {
    onMessage: async (message) => {
      console.log("SSE 流式数据:", message);
      await handleSSEMessage(message, fullContentRef, messageIndex);
    },
    onComplete: async () => {
      console.log("SSE 流式传输完成");
      // 处理最终内容渲染
      await immediateRender(async () => {
        if (questionContent.value.messages[messageIndex]) {
          questionContent.value.messages[messageIndex].content =
            await renderMarkdown(fullContentRef.value);
          scrollToBottom(questionContent.value.messages);
        }
      });
      sendMessage.value = "";
      anserLoading.value = false;
    },
  }
).catch((e) => {
  console.log("AI报错", e);
  messageDialog.value?.showMessage(
    e.message || "AI 请求失败,请重试",
    "error"
  );
  anserLoading.value = false;
});

在业务层,我们主要关注:

  • 传递请求参数(用户信息、问题内容等)
  • 实现 onMessage 回调处理流式数据
  • 实现 onComplete 回调处理流结束逻辑
  • 错误处理和加载状态管理

Markdown渲染集成

renderMarkdown函数实现

js 复制代码
import marked from "marked";

const renderMarkdown = async (item) => {
  try {
    const html = await marked.parse(item, {
      gfm: true,
      breaks: false,
      pedantic: false,
    });
    return html;
  } catch (err) {
    console.error("Markdown 渲染失败:", err);
    return `渲染错误: ${err.message}`;
  }
};
  

为什么 onMessage 回调不执行

许多开发者在实现 SSE 时都会遇到 onMessage 回调不执行的问题。结合上述代码,我们来分析可能的原因和解决方案。

1. 请求头设置不正确

问题:缺少 Accept: "text/event-stream" 请求头,或设置了错误的 Content-Type。

解决:确保请求头包含:

js 复制代码
header: {
  "Content-Type": "application/json",
  "Accept": "text/event-stream", // 这个请求头至关重要
}

2. 分块传输未启用

问题:未设置 enableChunked: true,导致无法接收流式数据。

解决:在 uni.request 中显式开启分块传输:

js 复制代码
enableChunked: true,  // 必须开启分块传输
responseType: "stream",  // 流类型响应

3. 数据解析逻辑错误

问题:缓冲区处理不当,导致消息无法正确分割。

解决:检查缓冲区处理逻辑:

js 复制代码
// 正确的消息分割逻辑
const messages = buffer.split("\n\n");
buffer = messages.pop() || "";
messages.forEach((message) => {
  if (message.trim()) {
    options.onMessage(message);
  }
});

4. 服务端数据格式错误

问题:服务端返回的不是标准的 SSE 格式数据。

解决:使用浏览器开发者工具的 Network 面板检查响应:

  • 确认响应头 Content-Type 为 text/event-stream
  • 确认响应体格式符合 SSE 规范
  • 检查是否有跨域等网络问题

5. 回调函数传递错误

问题:API 封装或调用时,回调函数传递路径不正确。

解决:跟踪回调函数的传递路径,确保:

js 复制代码
// API封装时正确接收回调
export const questionAPI = (data, { onMessage, onComplete }) => {
  return request({
    // ...其他参数
    onMessage,  // 正确传递回调
    onComplete
  });
};

SSE 调试技巧与工具

浏览器开发者工具

现代浏览器的开发者工具提供了对 SSE 的良好支持:

  1. Network 面板

    • 找到类型为 event-stream 的请求
    • 查看 "Response" 标签可实时看到 SSE 数据流
    • "Headers" 标签可检查请求头和响应头是否正确
  2. Console 面板

    • 使用 console.log 打印原始数据块
    • 记录缓冲区状态变化

实用调试代码片段

在 onChunkReceived 回调中添加详细日志:

js 复制代码
// 调试用:打印接收到的原始数据
console.log("原始数据块:", chunk);
// 调试用:打印缓冲区状态
console.log("缓冲区状态:", buffer);
// 调试用:打印分割后的消息数量
console.log("分割出的消息数:", messages.length);

常见问题排查清单

  1. 网络层面

    • 确认服务端是否支持 CORS
    • 检查请求是否成功建立连接
    • 查看响应状态码是否为 200
  2. 数据层面

    • 确认服务端是否持续发送数据
    • 检查数据格式是否符合 SSE 规范
    • 验证消息分隔符是否正确
  3. 代码层面

    • 检查回调函数是否正确传递
    • 确认分块处理逻辑是否正确
    • 验证错误处理是否覆盖所有情况

SSE vs WebSocket:如何选择

SSE 和 WebSocket 都可以实现实时通信,但它们各有适用场景:

表格

复制

特性 Server-Sent Events WebSocket
协议 HTTP 独立的 WebSocket 协议
连接 单向(服务器到客户端) 双向
开销
实现复杂度 简单 复杂
自动重连 内置支持 需要手动实现
数据格式 文本(UTF-8) 二进制/文本

选择建议

  • 如果你需要单向实时更新(如股票行情、新闻推送、AI 对话),选择 SSE
  • 如果你需要双向实时通信(如在线游戏、即时通讯),选择 WebSocket
  • 如果你希望快速开发兼容性好,选择 SSE
  • 如果你需要全双工通信高性能,选择 WebSocket

总结与展望

Server-Sent Events 是一种简单而强大的实时通信技术,特别适合需要服务器向客户端单向推送数据的场景。通过本文的代码解析,我们了解了如何在 Vue 项目中实现 SSE,包括 API 封装、分块处理和回调机制等核心要点。

随着 AI 应用的普及,SSE 技术将在更多场景中发挥重要作用。掌握 SSE 的前端实现,不仅能帮助我们构建更好的用户体验,也能为理解更复杂的实时通信技术打下基础。

希望本文能帮助你解决 SSE 实现中的困惑,如果你有其他问题或更好的实践经验,欢迎在评论区分享!

相关推荐
sean聊前端1 小时前
听说vite要一统江湖了,我看看怎么个事
前端
喝二两啤酒1 小时前
手把手打通 H5 多支付通道(Apple pay、Google pay、第三方卡支付)
前端
gongzemin1 小时前
约课小程序增加候补功能
前端·微信小程序·小程序·云开发
西西西西胡萝卜鸡1 小时前
徽标(Badge)的实现与优化铁壁猿版(简易版)
前端
王大宇_1 小时前
虚拟列表从入门到出门
前端·javascript
程序猿小蒜2 小时前
基于springboot的人口老龄化社区服务与管理平台
java·前端·spring boot·后端·spring
用户21411832636022 小时前
Google Nano Banana Pro图像生成王者归来
前端
文心快码BaiduComate2 小时前
下周感恩节!文心快码助力感恩节抽奖页快速开发
前端·后端·程序员
_小九2 小时前
【开源】耗时数月、我开发了一款功能全面的AI图床
前端·后端·图片资源