从文本到图像:SSE 如何助力 AI 内容实时呈现?(Typescript篇)
<style>#div_digg { float: right; font-size: 12px; margin: 10px; text-align: center; width: 120px; position: fixed; right: 0; bottom: 0; z-index: 10; background-color: rgba(255, 255, 255, 1); padding: 10px; border: 1px solid rgba(204, 204, 204, 1) }#cnblogs_post_body pre code span { font-family: Consolas, monospace }
#blogTitle>h2 { font-family: Consolas, monospace }
#blog-news { font-family: Consolas, monospace }
#topics .postTitle a { font-family: Georgia, Times New Roman, Times, sans-serif, monospace; font-weight: bold }
#cnblogs_post_body p { margin: 18px auto; color: rgba(0, 0, 0, 1); font-family: Georgia, Times New Roman, Times, sans-serif, monospace; font-size: 16px; text-indent: 0 }
#cnblogs_post_body h1 { font-family: Georgia, Times New Roman, Times, sans-serif, monospace; font-size: 32px; font-weight: bold; line-height: 1.5; margin: 10px 0 }
#cnblogs_post_body h2 { font-family: Consolas, "Microsoft YaHei", monospace; font-size: 26px; font-weight: bold; line-height: 1.5; margin: 20px 0 }
#cnblogs_post_body h3 { font-family: Georgia, Times New Roman, Times, sans-serif, monospace; font-size: 20px; font-weight: bold; line-height: 1.5; margin: 10px 0 }
#cnblogs_post_body h4 { font-family: Georgia, Times New Roman, Times, sans-serif, monospace; font-size: 18px; font-weight: bold; margin: 10px 0 }
em { font-style: normal; color: rgba(0, 0, 0, 1) }
#cnblogs_post_body ul li { font-family: Georgia, Times New Roman, Times, sans-serif, monospace; color: rgba(0, 0, 0, 1); font-size: 16px; list-style-type: disc }
#cnblogs_post_body ol li { font-family: Georgia, Times New Roman, Times, sans-serif, monospace; color: rgba(0, 0, 0, 1); font-size: 16px; list-style-type: decimal }
#cnblogs_post_body a:link { text-decoration: none; color: rgba(0, 44, 153, 1) }
#topics .postBody blockquote { background: rgba(255, 243, 212, 1); border-top: none; border-right: none; border-bottom: none; border-left: 5px solid rgba(246, 183, 60, 1); margin: 0; padding-left: 10px }
.cnblogs-markdown code { font-family: Consolas, "Microsoft YaHei", monospace !important; font-size: 16px !important; line-height: 1.8; background-color: rgba(245, 245, 245, 1) !important; border: none !important; padding: 0 5px !important; border-radius: 3px !important; margin: 1px 5px; vertical-align: middle; display: inline-block }
.cnblogs-markdown .hljs { font-family: Consolas, "Microsoft YaHei", monospace !important; font-size: 16px !important; line-height: 1.5 !important; padding: 5px !important }
#cnblogs_post_body h1 code, #cnblogs_post_body h2 code { font-size: inherit !important; border: none !important }</style>
<h1>从文本到图像:SSE 如何助力 AI 内容实时呈现?(Typescript篇)</h1>
<h2>前言</h2>
<p>在这个人工智能大模型日益普及的时代,AI 的能力从最初的简单文本回复,发展到了生成图像,甚至可以实时输出思考过程。那么,问题来了:这些多样化的数据是如何高效地从后端传递到前端的呢?今天,我们就来聊聊一种轻量级、简单又实用的技术——SSE(Server-Sent Events)。</p>
<h2>SSE(server-sent events)</h2>
<p>一句话概括: SSE(Server-Sent Events)是一种基于 HTTP 的轻量级协议,允许服务端通过长连接向客户端单向实时推送结构化文本数据流。</p>
<h3>它有哪些特点?</h3>
<ul>
<li>简单易用:前端和后端代码实现起来非常简单。</li>
<li>长连接:使用 HTTP 持久连接,适合持续推送数据。</li>
<li>单向通信:服务端推送,前端接收,不支持前端主动发消息。</li>
<li>轻量高效:相比 WebSocket 更加轻量。</li>
</ul>
<h3>JSON返回 vs SSE vs WebSocket 有什么区别</h3>
<p>JSON 返回:</p>
<pre class="highlighter-hljs"><code>const response = await fetch('https://');
await response.json();</code></pre>
<p>流式返回:</p>
<pre class="highlighter-hljs"><code>const response = await fetch('https://');
const reader = response.body?.getReader();
while (true) {
const { value, done } = await reader.read();
}</code></pre>
<p>WebSocket:</p>
<pre class="highlighter-hljs"><code>const socket = new WebSocket('ws://');
socket.onopen = () => {};
socket.onmessage= () => {};</code></pre>
<table>
<thead>
<tr><th>特性</th><th><code>response.json()</code></th><th><code>ReadableStream</code></th><th><code>WebSocket</code></th></tr>
</thead>
<tbody>
<tr>
<td><strong>处理方式</strong></td>
<td>全量读取,自动 JSON 解析</td>
<td>按块(chunk)逐步读取响应体,手动处理</td>
<td>双向通信:可持续接收和发送消息</td>
</tr>
<tr>
<td><strong>内存占用</strong></td>
<td>可能较高</td>
<td>较低</td>
<td>取决于消息频率和大小,但通常开销较低</td>
</tr>
<tr>
<td><strong>复杂性</strong></td>
<td>简单</td>
<td>相对复杂</td>
<td>需要手动处理连接、消息事件、错误等</td>
</tr>
<tr>
<td><strong>适用场景</strong></td>
<td>小到中等大小 JSON 响应</td>
<td>大型文件、实时数据、非 JSON 数据</td>
<td>实时双向通信场景,例如聊天应用、在线游戏等</td>
</tr>
<tr>
<td><strong>实时性</strong></td>
<td>无法实时</td>
<td>可以通过流式返回实现接近实时</td>
<td>原生支持实时通信,延迟低</td>
</tr>
<tr>
<td><strong>协议</strong></td>
<td>HTTP</td>
<td>HTTP</td>
<td>WebSocket(基于 HTTP 升级的全双工协议)</td>
</tr>
<tr>
<td><strong>连接状态</strong></td>
<td>每次请求独立连接</td>
<td>每次请求独立连接</td>
<td>长连接:连接建立后可持续使用</td>
</tr>
<tr>
<td><strong>服务端推送</strong></td>
<td>不支持</td>
<td>不支持</td>
<td>原生支持:服务端主动推送消息到客户端</td>
</tr>
</tbody>
</table>
<h2>浅入浅出</h2>
<p><img src="https://img2024.cnblogs.com/blog/809672/202502/809672-20250224170244864-1464871895.avif"></p>
<p>我们通过一个简单的例子来了解服务端如何通过 SSE 向前端推送数据。</p>
<p>后端代码:</p>
<pre class="highlighter-hljs"><code>let cursor = 0;
while (cursor < text.content.length) {
const randomLength = Math.floor(Math.random() * 10) + 1;
// 从当前光标位置切片文本,生成一个块
const chunk = text.content.slice(cursor, cursor + randomLength);
cursor += randomLength;
// 将数据块以 SSE 格式发送到客户端
res.write(`data: ${chunk}\n\n`);
await sleep(100);
}
// 当所有数据发送完成时,发送一个特殊的结束标记
res.write('data: \n\n');
res.end();</code></pre>
<p>核心逻辑:</p>
<ul>
<li>通过 res.write 向客户端发送数据块(以 data: 开头,符合 SSE 格式)。</li>
<li>每次发送后稍作延迟(模拟数据生成的过程)。</li>
<li>发送完所有数据后,用 标记结束。</li>
</ul>
<p>前端代码:</p>
<pre class="highlighter-hljs"><code>const response = await fetch('/api/sse', {
method: 'POST',
});
if (!response.ok) return;
const reader = response.body?.getReader();
if (!reader) return;
// 初始化一个缓冲区,用于存储未处理的流数据
let buffer = '';
// 创建一个 TextDecoder,用于将流数据解码为字符串
const decoder = new TextDecoder();
while (true) {
// 从流中读取下一个块(chunk)
const { value, done } = await reader.read();
// 如果流读取完成(done 为 true),退出循环
if (done) {
break;
}
if (value) {
const chunk = decoder.decode(value, { stream: true });
buffer += chunk;
// 按照双换行符(\n\n)将缓冲区拆分为多行
let lines = buffer.split('\n\n');
// 将最后一行(可能是不完整的行)存回缓冲区,等待下一次读取补全
buffer = lines.pop() || '';
for (const line of lines) {
// 检查行是否以 'data: ' 开头,这是 SSE (Server-Sent Events) 的格式
if (line.startsWith('data: ')) {
const data = line.slice(6);
// 如果接收到的是特殊标记 '',说明数据流结束,直接返回
if (data === '') {
return;
}
setMessage((prev) => {
return (prev += data);
});
}
}
}
}</code></pre>
<p>核心逻辑:</p>
<ul>
<li>通过流式读取服务端返回的数据</li>
<li>流数据解码为字符串并解析 SSE 数据格式</li>
<li>接收到结束标记 结束</li>
</ul>
<p>有了基础实现之后,接下来我们看看一些稍微复杂一点的场景,比如:</p>
<ul>
<li>如何处理错误?</li>
<li>如何控制 SSE 请求的中断?</li>
<li>如何支持更复杂的数据结构,比如 JSON 格式?图片?</li>
</ul>
<h2>进阶</h2>
<p><img src="https://img2024.cnblogs.com/blog/809672/202502/809672-20250224170321648-370873480.avif"></p>
<p><img src="https://img2024.cnblogs.com/blog/809672/202502/809672-20250224170336869-200165999.avif"></p>
<ol>
<li>将 SSE 返回的数据结构需改为 JSON 格式</li>
</ol>
<pre class="highlighter-hljs"><code>{ "t": "返回类型", "r": "返回内容" }</code></pre>
<ol start="2">
<li>前端使用 AbortController 来控制是否结束当前请求(但是在实际使用过程中可能需要其他方案)</li>
</ol>
<pre class="highlighter-hljs"><code>const response = await fetch('/api/sse', {
signal: abortController.signal,
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ ...reqBody }),
});</code></pre>
<p>后端代码:</p>
<pre class="highlighter-hljs"><code>let cursor = 0;
writeBySSE(res, { t: SSEResultType.Image, r: data.imageUrl });
while (cursor < data.think.length) {
const randomLength = Math.floor(Math.random() * 10) + 1;
const chunk = data.think.slice(cursor, cursor + randomLength);
cursor += randomLength;
if (showSSEError && cursor > showErrorCount) {
writeBySSE(res, { t: SSEResultType.Error, r: '发生错误!' });
res.end();
}
writeBySSE(res, { t: SSEResultType.Think, r: chunk });
await sleep(50);
}</code></pre>
<p>前端代码:</p>
<pre class="highlighter-hljs"><code>for (const line of lines) {
if (line.startsWith('data: ')) {
const l = line.slice(6);
const data: SseResponseLine = JSON.parse(l);
if (data.t === SSEResultType.Image) {
setMessage((prev) => {
return { ...prev, image: data.r };
});
} else if (data.t === SSEResultType.Think) {
setMessage((prev) => {
const newThink = prev.think + data.r;
if (prev.think === newThink) return prev;
return { ...prev, think: newThink };
});
} else if (data.t === SSEResultType.Text) {
setMessage((prev) => {
const newContent = prev.content + data.r;
if (prev.content === newContent) return prev;
return { ...prev, content: newContent };
});
} else if (data.t === SSEResultType.Cancelled) {
setMessage((prev) => {
return { ...prev, isCancelled: true };
});
setIsSending(false);
} else if (data.t === SSEResultType.End) {
setIsSending(false);
} else if (data.t === SSEResultType.Error) {
setMessage((prev) => {
return { ...prev, errorMsg: data.r };
});
setIsSending(false);
}
}
}</code></pre>
<h2>实战:接入Deepseek大模型</h2>
<p><img src="https://img2024.cnblogs.com/blog/809672/202502/809672-20250224170408075-1390186739.avif"></p>
<p>源代码地址: Github</p>
<h2>总结</h2>
<p>SSE 是一种简单而有效的技术,特别适用于需要从服务器向客户端实时推送数据的场景。相对于 WebSocket,它更加轻量,实现也更简单。文章通过示例代码和视频演示,清晰地展示了 SSE 的基本原理和进阶用法,以及在实际项目中的应用。</p>
<h4>支持我们!</h4>
<p>本文来自 Sdcb Chats 部分代码,如果您觉得有帮助请在 GitHub 上 Star 我们!您的支持是我们前进的动力。</p>
<p>再次感谢您的支持,期待未来为您带来更多惊喜!</p><br><br>
来源:https://www.cnblogs.com/greywen/p/18734470
頁:
[1]