前端实现 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 实现中的困惑,如果你有其他问题或更好的实践经验,欢迎在评论区分享!

相关推荐
咬人喵喵4 分钟前
14 类圣诞核心 SVG 交互方案拆解(附案例 + 资源)
开发语言·前端·javascript
问君能有几多愁~17 分钟前
C++ 日志实现
java·前端·c++
咬人喵喵18 分钟前
CSS 盒子模型:万物皆是盒子
前端·css
2401_8603195224 分钟前
DevUI组件库实战:从入门到企业级应用的深度探索,如何快速应用各种组件
前端·前端框架
韩曙亮1 小时前
【Web APIs】元素滚动 scroll 系列属性 ② ( 右侧固定侧边栏 )
前端·javascript·bom·window·web apis·pageyoffset
珑墨1 小时前
【浏览器】页面加载原理详解
前端·javascript·c++·node.js·edge浏览器
LYFlied1 小时前
在AI时代,前端开发者如何构建全栈开发视野与核心竞争力
前端·人工智能·后端·ai·全栈
用户47949283569152 小时前
我只是给Typescript提个 typo PR,为什么还要签协议?
前端·后端·开源
程序员爱钓鱼2 小时前
Next.js SSR 项目生产部署全攻略
前端·next.js·trae
程序员爱钓鱼2 小时前
使用Git 实现Hugo热更新部署方案(零停机、自动上线)
前端·next.js·trae