从文本到图像:SSE 如何助力 AI 内容实时呈现?(Typescript篇)
#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 }
从文本到图像:SSE 如何助力 AI 内容实时呈现?(Typescript篇)
前言
在这个人工智能大模型日益普及的时代,AI 的能力从最初的简单文本回复,发展到了生成图像,甚至可以实时输出思考过程。那么,问题来了:这些多样化的数据是如何高效地从后端传递到前端的呢?今天,我们就来聊聊一种轻量级、简单又实用的技术——SSE(Server-Sent Events)。
SSE(server-sent events)
一句话概括: SSE(Server-Sent Events)是一种基于 HTTP 的轻量级协议,允许服务端通过长连接向客户端单向实时推送结构化文本数据流。
它有哪些特点?
- 简单易用:前端和后端代码实现起来非常简单。
- 长连接:使用 HTTP 持久连接,适合持续推送数据。
- 单向通信:服务端推送,前端接收,不支持前端主动发消息。
- 轻量高效:相比 WebSocket 更加轻量。
JSON返回 vs SSE vs WebSocket 有什么区别
JSON 返回:
const response = await fetch('https://');
await response.json();
流式返回:
const response = await fetch('https://');
const reader = response.body?.getReader();
while (true) {
const { value, done } = await reader.read();
}
WebSocket:
const socket = new WebSocket('ws://');
socket.onopen = () => {};
socket.onmessage = () => {};
特性 | response.json() |
ReadableStream |
WebSocket |
---|---|---|---|
处理方式 | 全量读取,自动 JSON 解析 | 按块(chunk)逐步读取响应体,手动处理 | 双向通信:可持续接收和发送消息 |
内存占用 | 可能较高 | 较低 | 取决于消息频率和大小,但通常开销较低 |
复杂性 | 简单 | 相对复杂 | 需要手动处理连接、消息事件、错误等 |
适用场景 | 小到中等大小 JSON 响应 | 大型文件、实时数据、非 JSON 数据 | 实时双向通信场景,例如聊天应用、在线游戏等 |
实时性 | 无法实时 | 可以通过流式返回实现接近实时 | 原生支持实时通信,延迟低 |
协议 | HTTP | HTTP | WebSocket(基于 HTTP 升级的全双工协议) |
连接状态 | 每次请求独立连接 | 每次请求独立连接 | 长连接:连接建立后可持续使用 |
服务端推送 | 不支持 | 不支持 | 原生支持:服务端主动推送消息到客户端 |
浅入浅出
我们通过一个简单的例子来了解服务端如何通过 SSE 向前端推送数据。
后端代码:
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: [DONE]\n\n');
res.end();
核心逻辑:
- 通过 res.write 向客户端发送数据块(以 data: 开头,符合 SSE 格式)。
- 每次发送后稍作延迟(模拟数据生成的过程)。
- 发送完所有数据后,用 [DONE] 标记结束。
前端代码:
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);
// 如果接收到的是特殊标记 '[DONE]',说明数据流结束,直接返回
if (data === '[DONE]') {
return;
}
setMessage((prev) => {
return (prev += data);
});
}
}
}
}
核心逻辑:
- 通过流式读取服务端返回的数据
- 流数据解码为字符串并解析 SSE 数据格式
- 接收到结束标记 [DONE] 结束
有了基础实现之后,接下来我们看看一些稍微复杂一点的场景,比如:
- 如何处理错误?
- 如何控制 SSE 请求的中断?
- 如何支持更复杂的数据结构,比如 JSON 格式?图片?
进阶
- 将 SSE 返回的数据结构需改为 JSON 格式
{ "t": "返回类型", "r": "返回内容" }
- 前端使用 AbortController 来控制是否结束当前请求(但是在实际使用过程中可能需要其他方案)
const response = await fetch('/api/sse', {
signal: abortController.signal,
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ ...reqBody }),
});
后端代码:
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);
}
前端代码:
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);
}
}
}
实战:接入Deepseek大模型
源代码地址: Github
总结
SSE 是一种简单而有效的技术,特别适用于需要从服务器向客户端实时推送数据的场景。相对于 WebSocket,它更加轻量,实现也更简单。文章通过示例代码和视频演示,清晰地展示了 SSE 的基本原理和进阶用法,以及在实际项目中的应用。
支持我们!
本文来自 Sdcb Chats 部分代码,如果您觉得有帮助请在 GitHub 上 Star 我们!您的支持是我们前进的动力。
再次感谢您的支持,期待未来为您带来更多惊喜!
从文本到图像:SSE 如何助力 AI 内容实时呈现?(Typescript篇)的更多相关文章
- 【NLP】Tika 文本预处理:抽取各种格式文件内容
Tika常见格式文件抽取内容并做预处理 作者 白宁超 2016年3月30日18:57:08 摘要:本文主要针对自然语言处理(NLP)过程中,重要基础部分抽取文本内容的预处理.首先我们要意识到预处理的重 ...
- Web开发——HTML基础(图像、音频和视频内容)
参考: 参考:HTML中的图像 参考:视频和音频内容 目录: 1.HTML中的图像 1.1 我们如何在网页上放置图像? (1)替代文字(alt) (2)宽度和高度 (3)图片标题 1.2 用图形和图形 ...
- c# 利用AForge和百度AI开发实时人脸识别
baiduAIFaceIdentify项目是C#语言,集成百度AI的SDK利用AForge开发的实时人脸识别的小demo,里边包含了人脸检测识别,人脸注册,人脸登录等功能 人脸实时检测识别功能 思路是 ...
- JS和vue文本框输入改变p标签的内容测试
文本框输入,p标签的内容自动变成文本框的内容,如下是三种方法的测试: 方法1:JS里的onchange,当文本框内容改变事件,该事件里写的方法是,获取p标签本身,然后获取文本框的值,赋值给变量,最后给 ...
- 通AI启示录,从一篇数学物理基础论文说起 原创: 关注前沿科技 量子位 今天 允中 发自 凹非寺
通AI启示录,从一篇数学物理基础论文说起 原创: 关注前沿科技 量子位 今天 允中 发自 凹非寺
- bat批处理 查找替换:批处理如何查找并替换文本里特定字符串中的部分内容
批处理如何查找并替换文本里特定字符串中的部分内容 摘自:http://www.bathome.net/thread-43349-1-1.html 脚本如下: @if()==() echo off &a ...
- SVG 2D入门3 - 文本与图像
SVG中渲染文本 SVG的强大能力之一是它可以将文本控制到标准HTML页面不可能有的程度,而无须求助图像或其它插件.任何可以在形状或路径上执行的操作(如绘制或滤镜)都可以在文本上执行.尽管SVG的文本 ...
- AndroidRichText 让Textview轻松的支持富文本(图像ImageSpan、点击效果等等类似QQ微信聊天)
代码地址:https://github.com/Luction/AndroidRichText AndroidRichText帮助实现像QQ,微信一样的,一个TextView里既有文字又有表情又有图片 ...
- 荣耀实锤Magic2或将助力AI,再次带动成长?
临近年底,热闹了一年的手机圈纷纷偃旗息鼓,准备为明年3月的新品发力.然而今天(12月7日),恰逢节气大雪,@荣耀手机 在微博发布了一张预热海报,随后荣耀总裁赵明转发这条微博表示「关于技术,真的有很多话 ...
- 第 3 章 HTML5 网页中的文本和图像
文字和图像是网页中最主要.最常用的元素. 在互联网高速发展的今天,网站已经成为一个展示与宣传自我的通信工具(公司或个人可以通过网站介绍公司的服务与产品或介绍自己).这些都离不开网站中的网页,而网页的内 ...
随机推荐
- 知乎 微软当年的 J++ 究竟是什么?为什么 Sun 要告它? zz
https://www.zhihu.com/question/22814456 考个古
- 如何调整Gitlab-Runner最大并发数?
概述: 我们在使用gitlab-runner做cicd时,如果安装之后没有配置gitlab-runner的最大并发数,在使用时候可能会碰到job的警告(job日志超过字节限制):job's log e ...
- Linux&shell通过正则表达式查找文件练习
linux&shell编程中通过正则表达式来匹配查找文件极大的提高查找效率. 首先,解释一下下边出现的命令. linux部分: ls:查看文件夹内的命令. |:管道. grep:搜索后边匹配的 ...
- 生产环境Sentinel改造实践(二):规则管理推送改造
前文介绍了Sentinel相关的核心概念,本文开始动手对规则管理推送进行改造. 这里挑选流控规则模式改造为示例 Sentinel Dashboard 改造 在com.alibaba.csp.senti ...
- IM群聊消息的已读未读功能在存储空间方面的实现思路探讨
1.引言 IM系统中,特别是在企业应用场景下,消息的已读未读状态是一个强需求. 以阿里的钉钉为例,钉钉的产品定位是用于商务交流,其"强制已读回执"功能,让职场人无法再"假 ...
- 解决layer在移动端关闭按钮显示一半的问题
问题描述 layer弹出iframe,如果设置title为false,会自动设置closeBtn为2,也就是向右上方偏移了-28px,如果显示区域大于1100则正常,如果小于1100则会添加一段css ...
- 论文解读 原苏木素A通过靶向ACSL4/FTH1轴依赖性铁凋亡保护阿霉素诱导的心肌损伤和心功能障碍
同学们,大家好!今天给大家介绍一篇研究性论文,原苏木素A通过靶向ACSL4/FTH1轴依赖性铁凋亡保护阿霉素诱导的心肌损伤和心功能障碍,想了解这方面的同学们可以重点关注一下.这篇文章是2024年7月份 ...
- 封装的DynamicCRM平台中最实用的JS工具类
包含了一个遮罩层的使用对象和一个通用的CRM平台JS操作对象. 使用示例: 常用的比如去除页面查找字段guid的'{}':commonUtil.delBrackets(commonUtil.getLo ...
- 一文读懂 es6 中class方法中的this绑定
一直以来有这么个疑问? class 的方法中没有自动的绑定this 为什么手动绑定之后,在绑定之后的方法里调用class中的其他的方法(这个方法为什么就能使用this,不也应该是null或者undef ...
- linux:搭建Drupal
了解 Drupal 是使用 PHP 语言编写的开源内容管理框架(CMF),由内容管理系统(CMS)及 PHP 开发框架(Framework)共同构成.Drupal 具备强大的定制化开发能力,您可使用 ...