js-流式传输数据
总结前端流式传输数据的相关问题
一、核心结论
AI 对话文字逐步显示的核心是 「流式数据传输 + 前端逐段渲染」:后端以 “数据流(Stream)” 形式分批次返回文本片段,前端监听数据流的 “分片接收” 事件,逐段将文本插入 DOM,并配合定时器控制显示速度,模拟 “打字机” 效果;无后端时也可纯前端拆分文本、定时器逐字符渲染(用于演示)。
二、核心技术原理
| 环节 | 实现逻辑 |
|---|---|
| 后端 | 不再一次性返回完整文本,通过「HTTP 分块传输(Chunked)」/「SSE」/「WebSocket」分批次推送文本片段; |
| 前端 | 监听数据流接收事件(如 Fetch 的reader.read()、WebSocket 的onmessage),逐段解码并追加到 DOM; |
| 体验优化 | 定时器控制每段文本的显示间隔,模拟打字速度,支持暂停 / 继续、异常兜底等; |
三、分场景实现
场景 1:纯前端模拟(无后端,快速演示效果)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<div id="chat-content"></div>
<button onclick="simulateStreamText('您好!我是AI助手,很高兴为您解答问题。', 'chat-content', 80)">
触发流式显示
</button>
<script>
// 核心函数:模拟流式文本显示
function simulateStreamText(fullText, containerId, speed = 50) {
const container = document.getElementById(containerId);
container.textContent = ''; // 清空原有内容
let index = 0; // 记录当前渲染位置
// 递归逐字符渲染
function typeNextChar() {
if (index >= fullText.length) return; // 渲染完成
container.textContent += fullText[index]; // 追加单个字符
index++;
setTimeout(typeNextChar, speed); // 控制打字速度
}
typeNextChar();
}
</script>场景 2:Fetch + HTTP 流式响应(对接 AI API 主流方案,如 OpenAI)
① 前端代码(核心)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
async function streamChatResponse(prompt) {
const chatContainer = document.getElementById('chat-content');
chatContainer.textContent = '';
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt })
});
// 检查是否为流式响应
if (!response.body) throw new Error('非流式响应');
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8'); // 解码二进制流
// 循环读取流片段
while (true) {
const { done, value } = await reader.read();
if (done) break; // 流结束
// 解码二进制数据为文本片段
const chunk = decoder.decode(value, { stream: true });
// 模拟打字速度(可根据需求调整间隔)
await new Promise(resolve => setTimeout(resolve, 50));
chatContainer.textContent += chunk;
}
} catch (e) {
// 异常兜底:直接显示完整文本
chatContainer.textContent = '抱歉,响应失败:' + e.message;
}
}② 后端示例(Node.js/Express,模拟 AI 流式返回)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const express = require('express');
const app = express();
app.use(express.json());
// 流式聊天接口
app.post('/api/chat', (req, res) => {
// 开启分块传输(核心)
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.setHeader('Transfer-Encoding', 'chunked');
const { prompt } = req.body;
// 模拟AI生成文本的过程,分批次返回
const fullText = '您好!我是AI助手,您的问题是:' + prompt + ',我会逐步解答。';
const chunkSize = 2; // 每次返回2个字符
let index = 0;
// 定时推送文本片段
const interval = setInterval(() => {
if (index >= fullText.length) {
clearInterval(interval);
res.end(); // 结束流
return;
}
const chunk = fullText.slice(index, index + chunkSize);
res.write(chunk); // 写入分块数据
index += chunkSize;
}, 100);
});
app.listen(3000, () => console.log('后端启动:http://localhost:3000'));场景 3:WebSocket(实时双向交互,适合长连接聊天)
WebSocket 是全双工通信,后端可主动推送文本片段,适合需要 “实时交互” 的场景(如在线客服):
① 前端代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function initWebSocketChat() {
const chatContainer = document.getElementById('chat-content');
const ws = new WebSocket('ws://localhost:3000/ws/chat');
// 连接成功后发送提问
ws.onopen = () => ws.send(JSON.stringify({ prompt: '你好,介绍下自己' }));
// 接收后端推送的文本片段
ws.onmessage = (e) => {
const { content } = JSON.parse(e.data);
// 打字机效果
setTimeout(() => {
chatContainer.textContent += content;
}, 50);
};
// 连接关闭处理
ws.onclose = () => console.log('WebSocket连接关闭');
}② 后端示例(Node.js/ws 库)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 3000 });
wss.on('connection', (ws) => {
ws.on('message', (data) => {
const { prompt } = JSON.parse(data);
const fullText = `WebSocket流式响应:你问的是「${prompt}」,我正在逐步回复...`;
let index = 0;
// 逐字符推送
const interval = setInterval(() => {
if (index >= fullText.length) {
clearInterval(interval);
return;
}
ws.send(JSON.stringify({ content: fullText[index] }));
index++;
}, 50);
});
});四、对接OPENAI效果
OpenAI 返回的流式数据是data: {"content":"xx"}\n\n格式,需拆分解析:
1
2
3
4
5
6
7
8
9
// 处理OpenAI流式响应示例
const lines = chunk.split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
const data = line.replace(/^data: /, '');
if (data === '[DONE]') continue; // 忽略结束标记
const json = JSON.parse(data);
const content = json.choices[0].delta.content;
if (content) chatContainer.textContent += content;
}