手把手教你用@microsoft/fetch-event-source实现ChatGPT式流式对话(附完整代码)

从零构建类ChatGPT流式对话:基于@microsoft/fetch-event-source的现代前端实践

如果你正在开发一个AI对话应用,并且希望它拥有像ChatGPT那样流畅、逐字输出的体验,那么你很可能已经遇到了一个技术瓶颈:浏览器原生的EventSource API功能太弱了。它只支持GET请求,不能自定义请求头,更别提传递复杂的JSON数据体了。这就像给你一辆只能前进不能后退的汽车,在复杂的应用场景下寸步难行。

我在去年重构一个企业级AI助手项目时就遇到了这个问题。后端同事已经实现了完善的流式响应接口,但前端用EventSource调用时,因为无法传递上下文对话历史,导致AI每次回答都像失忆了一样。直到我发现了微软开源的@microsoft/fetch-event-source库,才真正解决了这个痛点。今天我就来分享如何用这个库,结合Vue 3的Composition API,打造一个完整的、生产可用的类ChatGPT对话界面。

1. 为什么选择fetch-event-source:超越原生EventSource的四大优势

在深入代码之前,我们先搞清楚这个库到底解决了什么问题。很多人以为SSE(Server-Sent Events)技术已经很成熟了,直接用EventSource不就行了吗?实际上,原生API有几个致命的限制:

1.1 请求方法的单一性 原生EventSource只支持GET方法。这意味着所有数据都必须通过URL参数传递。想象一下,你要发送一个包含10轮对话历史的上下文,每轮对话几百字,URL长度很快就会超过浏览器的限制(通常2000字符左右)。而fetch-event-source支持任何HTTP方法,包括POST、PUT等。

// 原生EventSource - 只能GET
const sse = new EventSource('/api/chat?context=很长很长的参数...');

// fetch-event-source - 支持POST和请求体
await fetchEventSource('/api/chat', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ messages: [...完整的对话历史] })
});

1.2 自定义请求头的缺失 现代Web应用几乎都需要身份验证,通常通过Authorization头传递token。原生EventSource不支持任何自定义请求头,这意味着你无法在SSE连接中携带认证信息。

1.3 重试策略的不可控 当连接断开时,浏览器会自动重试几次,然后默默放弃。你无法知道重试了多少次,也无法自定义重试逻辑。在生产环境中,这种黑盒行为是不可接受的。

1.4 响应处理的局限性 你无法在建立连接前对响应进行预处理。比如,如果网关返回了401错误,你希望先刷新token再重试,原生API做不到这一点。

@microsoft/fetch-event-source完美解决了这些问题,它基于现代的Fetch API构建,提供了完全兼容SSE协议但功能更强大的接口。

2. 环境搭建与基础配置

让我们从最基础的开始。假设你正在使用Vue 3 + TypeScript + Vite构建项目。

2.1 安装依赖 首先安装必要的包:

npm install @microsoft/fetch-event-source
# 如果你使用TypeScript,类型定义已经包含在包中

2.2 创建基础工具函数 我习惯先创建一个专门处理SSE连接的工具函数,这样可以在多个组件中复用。创建一个src/utils/sse.ts文件:

import { fetchEventSource, EventSourceMessage } from '@microsoft/fetch-event-source';

// 自定义错误类型,用于区分可重试和不可重试的错误
export class RetriableError extends Error {
  constructor(message?: string) {
    super(message);
    this.name = 'RetriableError';
  }
}

export class FatalError extends Error {
  constructor(message?: string) {
    super(message);
    this.name = 'FatalError';
  }
}

// SSE内容类型常量
export const EventStreamContentType = 'text/event-stream';

// 基础配置接口
export interface SSEClientOptions {
  url: string;
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
  headers?: Record<string, string>;
  body?: any;
  onOpen?: (response: Response) => Promise<void> | void;
  onMessage: (msg: EventSourceMessage) => void;
  onClose?: () => void;
  onError?: (err: any) => void;
  signal?: AbortSignal;
  openWhenHidden?: boolean;
  fetch?: typeof fetch;
}

/**
 * 创建SSE连接的通用函数
 */
export async function createSSEConnection(options: SSEClientOptions): Promise<() => void> {
  const {
    url,
    method = 'POST',
    headers = {},
    body,
    onOpen,
    onMessage,
    onClose,
    onError,
    signal,
    openWhenHidden = true,
    fetch: customFetch
  } = options;

  // 准备请求头
  const requestHeaders = {
    'Content-Type': 'application/json',
    ...headers
  };

  // 创建AbortController用于手动取消
  const abortController = signal ? undefined : new AbortController();
  const actualSignal = signal || abortController?.signal;

  try {
    await fetchEventSource(url, {
      method,
      headers: requestHeaders,
      body: body ? JSON.stringify(body) : undefined,
      signal: actualSignal,
      openWhenHidden,
      fetch: customFetch,
      
      async onopen(response) {
        console.log('SSE连接已建立', response);
        
        // 自定义连接验证逻辑
        if (onOpen) {
          await onOpen(response);
          return;
        }
        
        // 默认验证逻辑
        if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
          return; // 一切正常
        } else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
          // 客户端错误,通常是不可重试的
          throw new FatalError(`客户端错误: ${response.status}`);
        } else {
          // 服务器错误或网络问题,可以重试
          throw new RetriableError(`服务器错误: ${response.status}`);
        }
      },
      
      onmessage(msg) {
        // 这里可以添加全局的消息处理逻辑
        // 比如检查特定的错误事件
        if (msg.event === 'error') {
          throw new FatalError(`服务器返回错误: ${msg.data}`);
        }
        
        // 调用用户定义的消息处理器
        onMessage(msg);
      },
      
      onclose() {
        console.log('SSE连接已关闭');
        if (onClose) {
          onClose();
        }
        
        // 如果连接意外关闭,可以触发重试
        // throw new RetriableError('连接意外关闭');
      },
      
      onerror(err) {
        console.error('SSE连接错误:', err);
        
        if (onError) {
          onError(err);
          return;
        }
        
        // 默认错误处理逻辑
        if (err instanceof FatalError) {
          throw err; // 抛出致命错误,停止重试
        }
        
        // 对于可重试错误,不抛出异常,库会自动重试
        // 可以返回一个重试间隔(毫秒)
        // return 5000; // 5秒后重试
      }
    });
  } catch (error) {
    console.error('SSE连接异常:', error);
    throw error;
  }
  
  // 返回一个取消函数
  return () => {
    abortController?.abort();
  };
}

这个工具函数提供了完整的错误处理、类型安全和可配置性。在实际项目中,我发现将错误分为可重试和不可重试两类非常重要,这能显著提升用户体验。

3. 构建完整的ChatGPT式对话组件

现在我们来创建一个完整的对话组件。我将使用Vue 3的<script setup>语法,但思路同样适用于React或其他框架。

3.1 组件基础结构 创建src/components/ChatInterface.vue

<template>
  <div class="chat-container">
    <!-- 消息列表区域 -->
    <div ref="messagesContainer" class="messages-container">
      <div v-for="message in messages" :key="message.id" 
           :class="['message', message.role]">
        <div class="avatar">
          {
  
  { message.role === 'user' ? '👤' : '🤖' }}
        </div>
        <div class="content">
          <!-- 对于AI的流式响应,使用单独的渲染 -->
          <template v-if="message.role === 'assistant' && message.isStreaming">
            <div class="streaming-text">
              {
  
  { message.content }}
              <span class="cursor">▋</span>
            </div>
          </template>
          <template v-else>
            {
  
  { message.content }}
          </template>
          <div class="timestamp">
            {
  
  { formatTime(message.timestamp) }}
          </div>
        </div>
      </div>
      
      <!-- 加载指示器 -->
      <div v-if="isLoading" class="thinking-indicator">
        <div class="dot-flashing"></div>
        <span>AI正在思考...</span>
      </div>
    </div>
    
    <!-- 输入区域 -->
    <div class="input-area">
      <textarea
        v-model="inputText"
        placeholder="输入您的问题..."
        :disabled="isLoading"
        @keydown.enter.exact.prevent="handleSubmit"
        rows="3"
      ></textarea>
      <button 
        @click="handleSubmit" 
        :disabled="isLoading || !inputText.trim()"
        class="send-button"
      >
        {
  
  { isLoading ? '发送中...' : '发送' }}
      </button>
    </div>
  </div>
</template>

<script setup lang="ts">
内容概要:本资源聚焦于配电网在发生故障后的两阶段鲁棒恢复研究,旨在提升电力系统在不确定性条件下的恢复能力与运行可靠性。研究采用两阶段优化方法,第一阶段进行预恢复决策,如网络重构、分布电源出力调整等,以最小化预期损失;第二阶段则针对实际发生的故障场景实施校正控制,利用鲁棒优化理论应对负荷波动、新能源出力不确定性等因素,确保恢复方案的可行性与强健性。资源提供了完整的Matlab代码实现,复现了相关顶刊研究成果,便于使用者深入理解模型构建、算法求解及仿真分析全过程。; 适合人群:具备电力系统分析、优化理论基础及Matlab编程能力的研究生、科研人员及电力行业工程师。; 使用场景及目标:① 学习并掌握配电网故障恢复的先进优化方法,特别是两阶段鲁棒优化模型的构建与应用;② 复现和验证顶刊论文中的算法,为自身科研工作提供技术参考和代码基础;③ 将所学方法拓展应用于微电网、主动配电网等新型电力系统的可靠性评估与优化调度研究。; 阅读建议:学习者应结合提供的Matlab代码,仔细研读模型的数学公与求解逻辑,重点关注不确定性建模、两阶段决策变量的设定以及鲁棒对等转换技巧。建议在掌握基础案例后,尝试修改参数或引入新的约束条件进行扩展研究,以深化理解并提升创新能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值