Vue 3 + TypeScript 封装 fetchEventSource 实现流式消息处理
下面是一个完整的 Vue 3 + TypeScript 实现,封装 fetchEventSource
用于处理服务器发送事件(SSE)并实时渲染流式内容到页面的方案。
1. 安装依赖
首先确保安装了 @microsoft/fetch-event-source
包:
npm install @microsoft/fetch-event-source
2. 封装 fetchEventSource
创建一个 useEventSource.ts
组合式函数:
// src/composables/useEventSource.ts
import { fetchEventSource } from '@microsoft/fetch-event-source';
import { ref, onUnmounted } from 'vue';
interface EventSourceOptions {
url: string;
method?: 'GET' | 'POST';
headers?: Record<string, string>;
body?: any;
onMessage?: (data: string) => void;
onOpen?: (response: Response) => void;
onError?: (err: any) => void;
}
export function useEventSource() {
const data = ref<string>('');
const error = ref<any>(null);
const isLoading = ref<boolean>(false);
let controller: AbortController | null = null;
const fetchStream = async (options: EventSourceOptions) => {
isLoading.value = true;
error.value = null;
data.value = '';
controller = new AbortController();
try {
await fetchEventSource(options.url, {
method: options.method || 'GET',
headers: {
'Content-Type': 'application/json',
...options.headers,
},
body: options.body ? JSON.stringify(options.body) : undefined,
signal: controller.signal,
onopen: async (response) => {
if (response.ok) {
options.onOpen?.(response);
return;
}
throw new Error(`Failed to open stream: ${response.status} ${response.statusText}`);
},
onmessage: (event) => {
if (event.data) {
const content = event.data;
data.value += content;
options.onMessage?.(content);
}
},
onerror: (err) => {
throw err;
},
onclose: () => {
isLoading.value = false;
}
});
} catch (err) {
error.value = err;
isLoading.value = false;
}
};
const abort = () => {
if (controller) {
controller.abort();
isLoading.value = false;
}
};
onUnmounted(() => {
abort();
});
return {
data,
error,
isLoading,
fetchStream,
abort,
};
}
3. 在组件中使用
创建一个组件来使用这个封装好的事件源:
<template>
<div class="event-source-demo">
<h2>SSE 流式消息演示</h2>
<button
@click="startStream"
:disabled="isLoading"
>
{{ isLoading ? '接收中...' : '开始接收流式消息' }}
</button>
<button
@click="abortStream"
:disabled="!isLoading"
class="abort-btn"
>
停止接收
</button>
<div v-if="error" class="error-message">
错误: {{ error.message }}
</div>
<div class="message-container">
<pre>{{ message }}</pre>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { useEventSource } from '@/composables/useEventSource';
export default defineComponent({
name: 'EventSourceDemo',
setup() {
const { data: message, error, isLoading, fetchStream, abort } = useEventSource();
const startStream = () => {
fetchStream({
url: '你的SSE接口地址',
method: 'POST', // 根据你的API需要调整
body: {
// 你的请求参数
prompt: "请生成一些流式内容"
},
onMessage: (chunk) => {
console.log('收到数据块:', chunk);
},
onOpen: (response) => {
console.log('连接已建立', response);
},
onError: (err) => {
console.error('发生错误:', err);
}
});
};
const abortStream = () => {
abort();
};
return {
message,
error,
isLoading,
startStream,
abortStream,
};
},
});
</script>
<style scoped>
.event-source-demo {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
button {
padding: 8px 16px;
margin-right: 10px;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.abort-btn {
background-color: #f56c6c;
}
.error-message {
color: #f56c6c;
margin: 10px 0;
}
.message-container {
margin-top: 20px;
padding: 15px;
border: 1px solid #eee;
border-radius: 4px;
min-height: 200px;
background-color: #f9f9f9;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
}
</style>
4. 高级功能扩展
处理 JSON 数据
如果服务器返回的是 JSON 数据流,可以修改 onmessage
处理逻辑:
onmessage: (event) => {
if (event.data) {
try {
const jsonData = JSON.parse(event.data);
// 处理jsonData
options.onMessage?.(jsonData);
} catch (err) {
console.error('解析JSON失败:', err);
}
}
},
聊天应用示例
对于聊天应用,可以这样使用:
const messages = ref<Array<{role: string, content: string}>>([]);
const startChat = () => {
fetchStream({
url: '/api/chat',
method: 'POST',
body: {
messages: messages.value
},
onMessage: (chunk) => {
// 查找或创建AI的回复消息
const aiMessage = messages.value.find(m => m.role === 'assistant') ||
{ role: 'assistant', content: '' };
if (!messages.value.includes(aiMessage)) {
messages.value.push(aiMessage);
}
aiMessage.content += chunk;
}
});
};
注意事项
- 跨域问题:确保你的服务器支持 CORS 并正确配置了 SSE 相关的头信息
- 错误处理:网络中断或服务器错误时需要妥善处理
- 性能考虑:对于大量数据,考虑使用虚拟滚动等技术优化渲染性能
- 内存管理:长时间运行的连接可能导致内存增长,需要适当清理
这个实现提供了完整的流式消息处理方案,包括开始、停止、错误处理和实时渲染功能,可以根据具体需求进行进一步定制。