#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 格式?图片?

进阶

  1. 将 SSE 返回的数据结构需改为 JSON 格式
{ "t": "返回类型", "r": "返回内容" }
  1. 前端使用 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篇)的更多相关文章

  1. 【NLP】Tika 文本预处理:抽取各种格式文件内容

    Tika常见格式文件抽取内容并做预处理 作者 白宁超 2016年3月30日18:57:08 摘要:本文主要针对自然语言处理(NLP)过程中,重要基础部分抽取文本内容的预处理.首先我们要意识到预处理的重 ...

  2. Web开发——HTML基础(图像、音频和视频内容)

    参考: 参考:HTML中的图像 参考:视频和音频内容 目录: 1.HTML中的图像 1.1 我们如何在网页上放置图像? (1)替代文字(alt) (2)宽度和高度 (3)图片标题 1.2 用图形和图形 ...

  3. c# 利用AForge和百度AI开发实时人脸识别

    baiduAIFaceIdentify项目是C#语言,集成百度AI的SDK利用AForge开发的实时人脸识别的小demo,里边包含了人脸检测识别,人脸注册,人脸登录等功能 人脸实时检测识别功能 思路是 ...

  4. JS和vue文本框输入改变p标签的内容测试

    文本框输入,p标签的内容自动变成文本框的内容,如下是三种方法的测试: 方法1:JS里的onchange,当文本框内容改变事件,该事件里写的方法是,获取p标签本身,然后获取文本框的值,赋值给变量,最后给 ...

  5. 通AI启示录,从一篇数学物理基础论文说起 原创: 关注前沿科技 量子位 今天 允中 发自 凹非寺

    通AI启示录,从一篇数学物理基础论文说起 原创: 关注前沿科技 量子位 今天 允中 发自 凹非寺

  6. bat批处理 查找替换:批处理如何查找并替换文本里特定字符串中的部分内容

    批处理如何查找并替换文本里特定字符串中的部分内容 摘自:http://www.bathome.net/thread-43349-1-1.html 脚本如下: @if()==() echo off &a ...

  7. SVG 2D入门3 - 文本与图像

    SVG中渲染文本 SVG的强大能力之一是它可以将文本控制到标准HTML页面不可能有的程度,而无须求助图像或其它插件.任何可以在形状或路径上执行的操作(如绘制或滤镜)都可以在文本上执行.尽管SVG的文本 ...

  8. AndroidRichText 让Textview轻松的支持富文本(图像ImageSpan、点击效果等等类似QQ微信聊天)

    代码地址:https://github.com/Luction/AndroidRichText AndroidRichText帮助实现像QQ,微信一样的,一个TextView里既有文字又有表情又有图片 ...

  9. 荣耀实锤Magic2或将助力AI,再次带动成长?

    临近年底,热闹了一年的手机圈纷纷偃旗息鼓,准备为明年3月的新品发力.然而今天(12月7日),恰逢节气大雪,@荣耀手机 在微博发布了一张预热海报,随后荣耀总裁赵明转发这条微博表示「关于技术,真的有很多话 ...

  10. 第 3 章 HTML5 网页中的文本和图像

    文字和图像是网页中最主要.最常用的元素. 在互联网高速发展的今天,网站已经成为一个展示与宣传自我的通信工具(公司或个人可以通过网站介绍公司的服务与产品或介绍自己).这些都离不开网站中的网页,而网页的内 ...

随机推荐

  1. 知乎 微软当年的 J++ 究竟是什么?为什么 Sun 要告它? zz

    https://www.zhihu.com/question/22814456 考个古

  2. 如何调整Gitlab-Runner最大并发数?

    概述: 我们在使用gitlab-runner做cicd时,如果安装之后没有配置gitlab-runner的最大并发数,在使用时候可能会碰到job的警告(job日志超过字节限制):job's log e ...

  3. Linux&shell通过正则表达式查找文件练习

    linux&shell编程中通过正则表达式来匹配查找文件极大的提高查找效率. 首先,解释一下下边出现的命令. linux部分: ls:查看文件夹内的命令. |:管道. grep:搜索后边匹配的 ...

  4. 生产环境Sentinel改造实践(二):规则管理推送改造

    前文介绍了Sentinel相关的核心概念,本文开始动手对规则管理推送进行改造. 这里挑选流控规则模式改造为示例 Sentinel Dashboard 改造 在com.alibaba.csp.senti ...

  5. IM群聊消息的已读未读功能在存储空间方面的实现思路探讨

    1.引言 IM系统中,特别是在企业应用场景下,消息的已读未读状态是一个强需求. 以阿里的钉钉为例,钉钉的产品定位是用于商务交流,其"强制已读回执"功能,让职场人无法再"假 ...

  6. 解决layer在移动端关闭按钮显示一半的问题

    问题描述 layer弹出iframe,如果设置title为false,会自动设置closeBtn为2,也就是向右上方偏移了-28px,如果显示区域大于1100则正常,如果小于1100则会添加一段css ...

  7. 论文解读 原苏木素A通过靶向ACSL4/FTH1轴依赖性铁凋亡保护阿霉素诱导的心肌损伤和心功能障碍

    同学们,大家好!今天给大家介绍一篇研究性论文,原苏木素A通过靶向ACSL4/FTH1轴依赖性铁凋亡保护阿霉素诱导的心肌损伤和心功能障碍,想了解这方面的同学们可以重点关注一下.这篇文章是2024年7月份 ...

  8. 封装的DynamicCRM平台中最实用的JS工具类

    包含了一个遮罩层的使用对象和一个通用的CRM平台JS操作对象. 使用示例: 常用的比如去除页面查找字段guid的'{}':commonUtil.delBrackets(commonUtil.getLo ...

  9. 一文读懂 es6 中class方法中的this绑定

    一直以来有这么个疑问? class 的方法中没有自动的绑定this 为什么手动绑定之后,在绑定之后的方法里调用class中的其他的方法(这个方法为什么就能使用this,不也应该是null或者undef ...

  10. linux:搭建Drupal

    了解 Drupal 是使用 PHP 语言编写的开源内容管理框架(CMF),由内容管理系统(CMS)及 PHP 开发框架(Framework)共同构成.Drupal 具备强大的定制化开发能力,您可使用 ...