在前端開發中,處理 非流式數據 和 流式數據 的方式不同。根據是否完整接收數據、是否實時渲染的需求,可以分為以下四種典型場景:
一、四類常見場景總結
類型 | 數據完整性 | 是否實時渲染 | 適用技術/方法 |
---|---|---|---|
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>);
}