网站首页 > 技术文章 正文
前端流式输出
摘要
自chatGPT诞生以来,前端页面的流式输出,已经非常常见了。但凡涉及到大模型、AI智能体开发的项目,都离不开前端的流式输出。笔者查阅了很多资料后,总结了一下内容,给读者们展示一下各种流式输出的实现方法。
本文所采用的环境
node.js v18.20.1
koa 3.0
正文
传统的、标准的、典型的SSE
SSE(Server-Sent Events)是一种用于在浏览器中从服务器接收数据的技术。它允许服务器向浏览器推送数据,而不需要浏览器主动请求数据。
服务端关键
- 响应方法 get
- 响应头设置 Content-Type: text/event-stream
koa代码
import Router from "koa-router";
import {PassThrough} from 'node:stream'
const demo = new Router();
demo.get('/sse', async (ctx) => {
ctx.set({
"Content-Type": "text/event-stream;charset=utf-8",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
});
let stream = new PassThrough();
ctx.status = 200;
ctx.body = stream;
let str = '大家好,这里展示SSE流式输出的效果。';
sse(str,0,stream);
})
function sse(str,i,stream){
if(i === str.length) return stream.end();
stream.write('data: '+JSON.stringify({value:str[i]}));
stream.write('\n\n');
setTimeout(()=>{
sse(str,i+1,stream)
},Math.random() * 1000)
}
服务端注意事项
- SSE 消息体 每条消息以\n\n结束
- 允许自定义消息体,具体可参考EventSource
前端关键
- 使用EventSource客户端
- EventSource仅支持GET请求
- 服务端消息体要严格按照SSE规范输出
- 监听onmessage事件或者服务端自定义事件
前端代码
const source = new EventSource('http://localhost:3000/demo/sse');
source.onmessage = (e) => {
console.log(e.data);
};
前端注意事项
- 单向通信:SSE是一种单向通信协议,即服务器向客户端推送数据,如需双向通信,需要使用WebSocket。
- 自动重连:EventSource内置了自动重连功能,当连接断开时,会自动重新连接。在上面的例子中,当服务端stream.end()时,EventSource会自动重新连接。所以应当在合适的时机,前端主动断开连接,防止无限重连(source.close())。
总结
标准的EventSource实现简单,但仅支持GET请求,无法传输大量数据;不支持自定义Header,也无法实现鉴权。因此,使用场景有限,一般用来实现简单聊天应用、单纯从服务端推送数据到客户端的场景。
为了满足更多的需求,前端开始花式整活~
非标准SSE方式1-服务端按标准的SSE输出,浏览器端使用fetch
浏览器端代码
fetch('http://localhost:3000/demo/sse').then(async response => {
if (response.ok) {
let decoder = new TextDecoder('utf-8', {stream: true});
let reader = response.body?.getReader();
while (true) {
let {done, value} = await reader.read();
if (done) break;
let data = decoder.decode(value);
console.log('data:',data);
}
}
})
注意事项
- value 原始值是一个ArrayBuffer,需要使用TextDecoder进行解码。有兴趣的同学可以深入了解一下,加深对JS这门语音的理解。
非标准SSE方式2-服务端按标准的SSE输出,浏览器端使用普通的XMLHttpRequest
浏览器端代码
let xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:3000/demo/sse', true);
xhr.onprogress = e => {
console.log(xhr.responseText)
}
xhr.send();
同学们尝试一下,可以发现,这种方式返回的数据是历次消息体拼接后的结果。熟悉XMLHttpRequest的同学应该知道,这其实跟文件上传监听上传进度一样的道理,得到的是实时的进度,这样是不是就能理解为何的返回数据是历次消息体拼接后的结果了?
注意事项
- XMLHttpRequest并没有真正建立长连接,所以当消息超长时,会面临超时问题。所以,该方式实际是一种“伪流式传输”
- 由于返回的是历次消息体拼接后的结果,如需处理单个消息体,需要自己实现。
非标准SSE方式3-服务端按POST方式输出SSE,浏览器端使用(fetch/XMLHttpRequest)接收
此种方式前端代码和方式1、方式2一致,服务端改成POST方式输出SSE,实际效果一致,仅需把GET换成POST,可以支持前端大量数据的传输,可以传输大文件,不在赘述。
非标准SSE方式4-服务端按chunked方式输出(分块传输),浏览器端使用fetch接收
服务端代码
import Router from "koa-router";
import {PassThrough} from 'node:stream'
const demo = new Router();
demo.post('/chunked',async (ctx)=>{
ctx.set({
"Content-Type": "text/plain;charset=utf-8",
"Transfer-Encoding": "chunked",
"Connection": "keep-alive",
});
let stream = new PassThrough();
ctx.status = 200;
ctx.body = stream;
let str = '大家好,这里展示chunked流式输出的效果。';
chunked(str,0,stream);
});
function chunked(str,i,res){
if(i === str.length) return res.end();
res.write(str[i]+'\r\n');
setTimeout(()=>{
chunked(str,i+1,res)
},Math.random() * 1000)
}
前端代码
fetch('http://localhost:3000/demo/chunked',{
method: 'POST',
}).then(async response => {
if (response.ok) {
let decoder = new TextDecoder('utf-8', {stream: true});
let reader = response.body?.getReader();
while (true) {
let {done, value} = await reader.read();
if (done) break;
let data = decoder.decode(value);
console.log('data:',data);
}
}
})
注意事项
- chunked方式,服务端返回的响应头中,需要设置Transfer-Encoding: chunked,表示返回的是分块传输。
- 实际效果发现与SSE一样
- 仔细观察可发现,SSE中会默认设置响应头Transfer-Encoding: chunked
结尾
想要实现流式传输,最佳方案如下:
服务端按照不必严格按照SSE消息体方式传输,GET/POST均可,只需设置如下响应头即可。
- Content-Type: text/event-stream
- Connection: keep-alive
- Cache-Control: no-cache
- Transfer-Encoding: chunked
- 浏览器端使用fetch接收
如有更好的方案或者好玩的方案,欢迎留言。
- 上一篇: 前端基础进阶(一):内存空间详细图解
- 下一篇:已经是最后一篇了
猜你喜欢
- 2025-06-10 前端基础进阶(一):内存空间详细图解
- 2025-06-10 JavaScript数组中slice、concat方法真的是深拷贝吗?
- 2025-06-10 Set代替Array去重,实测性能对比(set方法数组去重)
- 2025-06-10 JavaScript去除数组重复元素的几种方法
- 2025-06-10 Vue短文:如何使用v-for反转数组的顺序?
- 2025-06-10 判断变量是否为数组(如何判断某变量是否为数组数据类型)
- 2025-06-10 JavaScript数组剖析(js 数组处理方法)
- 2024-09-30 JavaScript数组_数组方法「二」(二十七)
- 2024-09-30 table组件,前端如何使用table组件打印数组数据
- 2024-09-30 前端数组改字符串方法 前端数组改字符串方法是什么
你 发表评论:
欢迎- 503℃几个Oracle空值处理函数 oracle处理null值的函数
- 500℃Oracle分析函数之Lag和Lead()使用
- 496℃Oracle数据库的单、多行函数 oracle执行多个sql语句
- 491℃0497-如何将Kerberos的CDH6.1从Oracle JDK 1.8迁移至OpenJDK 1.8
- 483℃Oracle 12c PDB迁移(一) oracle迁移到oceanbase
- 474℃【数据统计分析】详解Oracle分组函数之CUBE
- 457℃最佳实践 | 提效 47 倍,制造业生产 Oracle 迁移替换
- 455℃Oracle有哪些常见的函数? oracle中常用的函数
- 最近发表
- 标签列表
-
- 前端设计模式 (75)
- 前端性能优化 (51)
- 前端模板 (66)
- 前端跨域 (52)
- 前端缓存 (63)
- 前端react (48)
- 前端aes加密 (58)
- 前端脚手架 (56)
- 前端md5加密 (54)
- 前端富文本编辑器 (47)
- 前端路由 (61)
- 前端数组 (73)
- 前端定时器 (47)
- Oracle RAC (73)
- oracle恢复 (76)
- oracle 删除表 (48)
- oracle 用户名 (74)
- oracle 工具 (55)
- oracle 内存 (50)
- oracle 导出表 (57)
- oracle 中文 (51)
- oracle链接 (47)
- oracle的函数 (57)
- 前端调试 (52)
- 前端登录页面 (48)
本文暂时没有评论,来添加一个吧(●'◡'●)