前端开发处理‘流式数据’与‘非流式数据’,在接收完整与非完整性数据时应该如何渲染和使用
在前端开发中,处理 非流式数据 和 流式数据 的方式不同。根据是否完整接收数据、是否实时渲染的需求,可以分为以下四种典型场景:
一、四类常见场景总结
类型 | 数据完整性 | 是否实时渲染 | 适用技术/方法 |
---|---|---|---|
A | 完整数据(一次性返回) | 否(等全部加载完) | fetch , axios , JSON.parse() |
B | 完整数据(一次性返回) | 是(模拟逐字显示) | setTimeout / requestAnimationFrame 模拟打字效果 |
C | 不完整数据(分段传输) | 否(等全部加载完) | buffer 缓存 + 最终解析 |
D | 不完整数据(分段传输) | 是(边接收边渲染) | ReadableStream , SSE , WebSocket , JSON 增量解析 |
场景对比表
场景 | 是否流式 | 是否实时渲染 | 是否需要增量解析 | 代表应用 |
---|---|---|---|---|
A | 非流式 | 否 | 不需要 | 获取静态数据 |
B | 非流式 | 是 | 不需要 | AI 回答展示 |
C | 流式 | 否 | 需要 | 日志聚合、大文件解析 |
D | 流式 | 是 | 需要 | AI 聊天机器人、代码生成器 |
具体情况和项目还需要具体分析,并非生搬这四种场景。以下为场景对比的详解与实现:
场景详解与实现方案
场景 A:完整数据 + 非实时渲染(最常见)
适用于传统 API 请求,如获取用户列表、文章内容等。
示例代码:
const response = await fetch('/api/data');
const data = await response.json();
render(data);
渲染逻辑:
function render(data) {document.getElementById('content').innerText = data.content;
}
场景 B:完整数据 + 实时渲染(模拟打字效果)
适用于需要“逐字显示”的视觉效果,比如 AI 对话界面。
示例代码:
const response = await fetch('/api/data');
const data = await response.json();simulateTyping(data.content, document.getElementById('output'), 50);
打字函数实现:
function simulateTyping(text, element, interval = 50) {let index = 0;const timer = setInterval(() => {if (index < text.length) {element.textContent += text[index++];} else {clearInterval(timer);}}, interval);
}
场景 C:不完整数据 + 非实时渲染(等待拼接完成)
适用于大文件下载、日志聚合等场景,需等到所有数据接收完毕后再统一处理。
示例代码:
let buffer = '';while (true) {const { done, value } = await reader.read();if (done) break;buffer += decoder.decode(value, { stream: true });
}// 等待完成后统一解析
const finalData = JSON.parse(buffer);
render(finalData);
场景 D:不完整数据 + 实时渲染(流式解析 + 边接收边展示)
适用于 AI 流式回复、聊天机器人、代码生成器 等场景,需边接收边解析边渲染。
核心流程:
- 接收 chunk 数据
- 拼接到 buffer
- 尝试增量解析
- 提取有效字段并更新 UI
示例代码(结合 jsonparse
):
import sax from 'jsonparse';let buffer = '';
const parser = new sax.Parser();parser.onValue = function (value) {if (this.stack.length === 1 && this.key === 'content') {updateUI(value); // 实时渲染 content 字段}
};async function processStream() {const response = await fetch('/api/stream-endpoint', {method: 'POST',body: JSON.stringify({ prompt: '讲个故事' })});const reader = response.body.getReader();const decoder = new TextDecoder();while (true) {const { done, value } = await reader.read();if (done) break;buffer += decoder.decode(value, { stream: true });try {parser.write(buffer);buffer = ''; // 成功解析后清空 buffer} catch (e) {// 忽略不完整 JSON 错误,继续等待更多数据}}parser.close();
}function updateUI(text) {const container = document.getElementById('output');container.textContent += text;
}
技术选型建议
使用场景 | 推荐技术 |
---|---|
非流式完整数据 | fetch , axios , JSON.parse() |
流式数据(HTTP SSE) | EventSource , fetch + ReadableStream |
WebSocket 数据流 | WebSocket , Socket.IO |
JSON 增量解析 | jsonparse , clarinet |
实时 UI 更新 | requestAnimationFrame , DOM diff , 节流控制 |
Vue / React 场景优化建议
Vue 示例(使用 ref
控制 DOM)
<template><div id="output">{{ outputText }}</div>
</template><script setup>
import { ref } from 'vue';
const outputText = ref('');function updateUI(text) {outputText.value += text;
}
</script>
React 示例(使用 useState
或 useRef
)
function ChatBox() {const [text, setText] = useState('');const containerRef = useRef(null);function updateUI(chunk) {setText(prev => prev + chunk);setTimeout(() => {containerRef.current.scrollTop = containerRef.current.scrollHeight;}, 0);}return (<div ref={containerRef} style={{ height: '300px', overflowY: 'auto' }}>{text}</div>);
}