大家好,我是庚云。在AI重塑前端开发的今天,AI应用框架前端工程师 正成为最具潜力的技术方向。要成为这个领域的专家,你需要掌握这些核心能力:
🚀 AI交互全链路开发能力
- 智能对话引擎封装(LLM Integration)
- 多模态输入/输出处理(Multi-modal IO)
- 可视化Agent工作流(Agent Visualization)
- 数据驱动UI架构(Data-driven Rendering)
今天,我们将深入其中最关键的交互技术------流式输出(Streaming) ,这是提升AI产品用户体验的胜负手!
🤔 一、什么是流式输出?为什么它这么香?
想象一下,你去餐厅点菜:
传统方式 :厨师做完整桌菜才一起端上来,你饿得前胸贴后背
流式输出:做好一道上一道,你可以边吃边等,体验爽翻天
流式输出就是数据一边生成一边传输,用户不用干等,看着内容逐渐出现,就像看直播一样过瘾!
🎯 二、适用场景
- AI对话:ChatGPT那种打字机效果

💪 三、3大方案深度对比
🌟 方案一:EventSource - 最简单但最"直男"
javascript
// 基本用法(GET请求专用)
const es = new EventSource('/api/chat');
es.onmessage = (e) => {
console.log(e.data); // 数据长这样 → "你好呀..."
};
优点:
- 浏览器原生支持,不需要额外库
- 自动重连,网络断了会自己恢复
缺点:
- 只支持GET请求(想POST?没门!😅)
- 不能自定义请求头(想带token?抱歉!)
- 只支持UTF-8编码
- IE直接扑街:没错,微软又不支持!🙄
🚀 方案二:Fetch API + ReadableStream - 最灵活但手酸
javascript
// 高级玩家必备(能POST、能传Header!)
const res = await fetch('/api/chat', {
method: 'POST',
headers: {'Token': '123'}
});
const reader = res.body.getReader(); // 拿到"水管龙头"
while (true) {
const { done, value } = await reader.read(); // 一勺一勺喝汤
if (done) break;
const text = new TextDecoder().decode(value); // 二进制转文字
console.log(text);
}
优点:
- 支持所有HTTP方法,能发POST !能加Header!自由度拉满!
- 现代浏览器都支持
- 连二进制流(比如PDF下载进度)都能处理!
缺点:
- 要自己管关闭流 、错误重试,代码写到你怀疑人生🤦♂️
🎨 方案三:fetch-event-source - 微软大佬的"轮椅"
javascript
// 企业级推荐!(自带重试、断线续传)
import { fetchEventSource } from '@microsoft/fetch-event-source';
await fetchEventSource('/api/chat', {
method: 'POST',
headers: { 'Token': '123' },
onmessage(msg) {
console.log(msg.data); // 真香!
}
});
优点:
- 结合了前两者的优点
- 支持POST请求和自定义头部
- 自动重连和错误处理
- 微软出品,质量有保证
缺点:
- 需要额外安装库
- 包体积稍大一点
🎯 四、技术选型决策树
sql
你的项目需求是什么?
├── 简单的GET请求推送 → EventSource
├── 需要POST请求/自定义头部
│ ├── 项目允许引入外部库 → fetch-event-source
│ └── 不允许外部依赖 → Fetch + ReadableStream
└── 需要精确控制流处理 → Fetch + ReadableStream
💁♂️tips:
- 流式输出经验少闭眼选
fetch-event-source
- 技术大佬选
Fetch + ReadableStream
🎁独家赠送:
- fetch-event-source 案例:京东 joyagent-jdgenie
- Fetch + ReadableStream 案例:dify chatbot-ui
💡 五、Vue/React里怎么用?
🔥 Vue 3 + fetch-event-source:响应式流数据处理
- 封装流式请求工具(sseUtils.js)
JavaScript
// src/utils/sseUtils.js
import { fetchEventSource } from '@microsoft/fetch-event-source';
/**
* 流式请求封装
* @param {string} url - 请求地址
* @param {Object} options - 配置选项
* @param {Object} options.body - 请求体
* @param {Object} options.headers - 请求头
* @param {function} options.onMessage - 消息处理回调
* @param {function} options.onOpen - 连接打开回调
* @param {function} options.onClose - 连接关闭回调
* @param {function} options.onError - 错误处理回调
* @returns {Promise} 返回一个可取消的Promise
*/
export const createSSEConnection = (url, {
body,
headers = {},
onMessage,
onOpen,
onClose,
onError
}) => {
// 创建一个AbortController用于取消请求
const ctrl = new AbortController();
// 返回一个包含取消方法的Promise
return {
promise: fetchEventSource(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...headers
},
body: JSON.stringify(body),
signal: ctrl.signal,
async onopen(response) {
if (response.ok) {
onOpen?.();
return; // 一切正常,继续
}
throw new Error(`Server error: ${response.status}`);
},
onmessage(msg) {
try {
// 如果数据是JSON格式则解析,否则直接使用
const data = msg.data ? JSON.parse(msg.data) : msg.data;
onMessage?.(data);
} catch (err) {
console.error('Failed to parse message data', err);
}
},
onclose() {
onClose?.();
},
onerror(err) {
onError?.(err);
throw err; // 重新抛出以停止重试
}
}),
cancel: () => ctrl.abort()
};
};
- 在 Vue3 组件中使用 (StreamComponent.vue)
javascript
<script setup>
import { ref, onUnmounted } from 'vue';
import { createSSEConnection } from '@/utils/sseUtils';
const streamData = ref(''); // 存储流式数据
const isLoading = ref(false); // 加载状态
const error = ref(null); // 错误信息
let sseConnection = null; // 存储SSE连接
// 发起流式请求
const startStream = async () => {
try {
// 重置状态
streamData.value = '';
isLoading.value = true;
error.value = null;
// 创建SSE连接
sseConnection = createSSEConnection('https://api.example.com/stream', {
body: { query: '获取流式数据' },
onMessage: (data) => {
// 企业技巧1: 使用函数式更新避免重复触发响应式
streamData.value += data;
// 企业技巧2: 自动滚动到底部 (适用于聊天场景)
nextTick(() => {
const container = document.getElementById('stream-container');
if (container) {
container.scrollTop = container.scrollHeight;
}
});
},
onOpen: () => {
console.log('连接已建立');
},
onClose: () => {
isLoading.value = false;
console.log('连接已关闭');
},
onError: (err) => {
isLoading.value = false;
error.value = err.message;
console.error('发生错误:', err);
}
});
await sseConnection.promise;
} catch (err) {
if (err.name !== 'AbortError') {
error.value = err.message;
}
} finally {
isLoading.value = false;
}
};
// 停止流式请求
const stopStream = () => {
if (sseConnection) {
sseConnection.cancel();
sseConnection = null;
}
};
// 组件卸载时自动取消请求
onUnmounted(() => {
stopStream();
});
// 企业技巧3: 使用计算属性处理流式数据
const processedStreamData = computed(() => {
return streamData.value
.replace(/\n/g, '<br>') // 换行转换
.replace(/\t/g, ' '); // 制表符转换
});
</script>
<template>
<div class="stream-container">
<h2>流式数据演示</h2>
<!-- 控制按钮 -->
<div class="controls">
<button @click="startStream" :disabled="isLoading">开始流式请求</button>
<button @click="stopStream" :disabled="!isLoading">停止</button>
</div>
<!-- 加载状态 -->
<div v-if="isLoading" class="loading">加载中...</div>
<!-- 错误显示 -->
<div v-if="error" class="error">{{ error }}</div>
<!-- 流式数据展示 -->
<div
id="stream-container"
class="stream-content"
v-html="processedStreamData"
></div>
<!-- 企业技巧4: 显示数据统计 -->
<div class="stats">
已接收: {{ streamData.length }} 字符 |
行数: {{ (streamData.match(/\n/g) || []).length + 1 }}
</div>
</div>
</template>
⚛️ React + Fetch ReadableStream:Hooks让一切变简单
javascript
import { useState, useEffect, useCallback } from 'react';
function useStreamData(url) {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const startStream = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch(url);
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = new TextDecoder().decode(value);
const lines = chunk.split('\n').filter(line => line.trim());
lines.forEach(line => {
if (line.startsWith('data: ')) {
const jsonData = JSON.parse(line.slice(6));
// 使用函数式更新,避免闭包陷阱
setData(prev => [...prev, jsonData]);
}
});
}
} catch (err) {
setError(err);
} finally {
setIsLoading(false);
}
}, [url]);
return { data, isLoading, error, startStream };
}
// 使用自定义Hook
function ChatComponent() {
const { data, isLoading, startStream } = useStreamData('/api/chat');
return (
<div>
<button onClick={startStream}>开始对话</button>
{data.map((msg, index) => (
<div key={index}>{msg.content}</div>
))}
</div>
);
}
🎯 六、框架集成的关键要点
Vue最佳实践:
streamData.value + data
增量更新,减少不必要的重新渲染- 使用计算属性处理流式数据
- 别忘了在
onUnmounted
里清理资源
React最佳实践:
- 用
useCallback
缓存函数,避免无限重渲染 - 用函数式更新
setData(prev => [...prev, newData])
,避免闭包问题 - 自定义Hook让逻辑复用更简单
💥 七、常见的坑,踩过的都懂
🕳️ 坑1:内存泄漏大户
javascript
// ❌ 错误示范:忘记关闭连接
function BadComponent() {
useEffect(() => {
const eventSource = new EventSource('/api/stream');
// 没有清理,组件卸载后连接还在!
}, []);
}
// ✅ 正确姿势:记得清理
function GoodComponent() {
useEffect(() => {
const eventSource = new EventSource('/api/stream');
return () => {
eventSource.close(); // 组件卸载时关闭连接
};
}, []);
}
🕳️ 坑2:移动端的无情背刺
javascript
// 移动端网络切换时,需要重新连接
function handleNetworkChange() {
window.addEventListener('online', () => {
// 网络恢复,重新连接
reconnectStream();
});
window.addEventListener('offline', () => {
// 网络断开,清理连接
closeStream();
});
}
🕳️ 坑3:CORS的老大难
javascript
// 后端需要设置正确的CORS头
// Access-Control-Allow-Origin: *
// Access-Control-Allow-Headers: Cache-Control
// Cache-Control: no-cache // SSE必须设置,不然浏览器会缓存
// 前端带认证token的正确姿势
const eventSource = new EventSource('/api/stream?token=your-token');
// 或者用fetch-event-source
fetchEventSource('/api/stream', {
headers: {
'Authorization': 'Bearer your-token'
}
});
🚀 八、高级玩法:让你的应用更丝滑
📊 大数据量流式渲染优化
当流数据量很大时(比如AI生成长文、实时日志),需要考虑性能优化:
javascript
// React实现:大数据量虚拟滚动
import { useState, useMemo, useCallback } from 'react';
function useLargeStreamRenderer(containerHeight = 400, itemHeight = 60) {
const [allMessages, setAllMessages] = useState([]);
const [scrollTop, setScrollTop] = useState(0);
// 计算可见区域
const visibleData = useMemo(() => {
const visibleCount = Math.ceil(containerHeight / itemHeight);
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - 5); // 5个缓冲
const endIndex = Math.min(allMessages.length - 1, startIndex + visibleCount + 10);
return {
items: allMessages.slice(startIndex, endIndex + 1),
startIndex,
totalHeight: allMessages.length * itemHeight,
offsetY: startIndex * itemHeight
};
}, [allMessages, scrollTop, containerHeight, itemHeight]);
const addMessage = useCallback((message) => {
setAllMessages(prev => {
const updated = [...prev, message];
// 超过1万条消息时,保留最新的8000条
return updated.length > 10000 ? updated.slice(-8000) : updated;
});
}, []);
const handleScroll = useCallback((e) => {
setScrollTop(e.target.scrollTop);
}, []);
return {
visibleData,
totalCount: allMessages.length,
addMessage,
handleScroll
};
}
🔄 智能重连策略
javascript
// 企业级重连管理器
class SmartReconnector {
constructor(options = {}) {
this.maxRetries = options.maxRetries || 5;
this.backoffStrategy = options.backoffStrategy || 'exponential';
this.baseDelay = options.baseDelay || 1000;
this.maxDelay = options.maxDelay || 30000;
this.retryCount = 0;
this.isConnecting = false;
}
async reconnect(connectFn) {
if (this.retryCount >= this.maxRetries) {
throw new Error('已达到最大重试次数');
}
const delay = this.calculateDelay();
await this.sleep(delay);
this.isConnecting = true;
this.retryCount++;
try {
await connectFn();
this.reset(); // 连接成功,重置计数
return true;
} catch (error) {
this.isConnecting = false;
throw error;
}
}
calculateDelay() {
switch (this.backoffStrategy) {
case 'linear':
return Math.min(this.baseDelay * this.retryCount, this.maxDelay);
case 'exponential':
return Math.min(this.baseDelay * Math.pow(2, this.retryCount), this.maxDelay);
default:
return this.baseDelay;
}
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
reset() {
this.retryCount = 0;
this.isConnecting = false;
}
}
🎉 总结
掌握了这些内容,你就能:
- 被面试官问到项目难点 ,直接把前文中的
流式请求组件封装
甩给他 - 面试时自信地讨论各种流式输出方案的优劣
- 项目里写出健壮、高性能的企业级代码
SSE流式输出不再是难题,而是你技术栈中的一把利器!🔥
想要完整源码和更多实战案例?私信我获取!包含Vue、React完整项目模板,开箱即用! 📨