什么是 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 的核心功能,主要包括:
- 设置正确的请求头:Accept: "text/event-stream" 是 SSE 的标志性请求头
- 开启分块传输:enableChunked: true 确保能够接收流式数据
- 分块数据处理:通过 onChunkReceived 事件监听数据块到达
- 数据缓冲区:使用 buffer 变量处理可能被分割的不完整数据块
- 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 的良好支持:
-
Network 面板:
- 找到类型为 event-stream 的请求
- 查看 "Response" 标签可实时看到 SSE 数据流
- "Headers" 标签可检查请求头和响应头是否正确
-
Console 面板:
- 使用 console.log 打印原始数据块
- 记录缓冲区状态变化
实用调试代码片段
在 onChunkReceived 回调中添加详细日志:
js
// 调试用:打印接收到的原始数据
console.log("原始数据块:", chunk);
// 调试用:打印缓冲区状态
console.log("缓冲区状态:", buffer);
// 调试用:打印分割后的消息数量
console.log("分割出的消息数:", messages.length);
常见问题排查清单
-
网络层面:
- 确认服务端是否支持 CORS
- 检查请求是否成功建立连接
- 查看响应状态码是否为 200
-
数据层面:
- 确认服务端是否持续发送数据
- 检查数据格式是否符合 SSE 规范
- 验证消息分隔符是否正确
-
代码层面:
- 检查回调函数是否正确传递
- 确认分块处理逻辑是否正确
- 验证错误处理是否覆盖所有情况
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 实现中的困惑,如果你有其他问题或更好的实践经验,欢迎在评论区分享!