Vue 3 + TypeScript 封装 fetchEventSource 实现流式消息处理

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;
    }
  });
};

注意事项

  1. 跨域问题:确保你的服务器支持 CORS 并正确配置了 SSE 相关的头信息
  2. 错误处理:网络中断或服务器错误时需要妥善处理
  3. 性能考虑:对于大量数据,考虑使用虚拟滚动等技术优化渲染性能
  4. 内存管理:长时间运行的连接可能导致内存增长,需要适当清理

这个实现提供了完整的流式消息处理方案,包括开始、停止、错误处理和实时渲染功能,可以根据具体需求进行进一步定制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值