在 React Umi 项目中实现基于 SSE 的流式聊天功能

在使用 ChatGPT 的过程中,AI 回复常常是逐字"打印"出来的,这种"流式输出"极大提升了交互体验。本篇文章将带你在基于 Umi 的 React 应用中,通过 Server-Sent Events(SSE) 实现这一流式聊天功能,并封装一个可复用的 Hook,简洁优雅。


🧠 什么是 SSE?

SSE(Server-Sent Events)是一种基于 HTTP 协议的单向通信方式,允许服务器持续向客户端推送事件。它适用于实时日志、在线通知、AI 输出等应用场景。

与 WebSocket 相比:

  • 更轻量,基于 HTTP
  • 单向(服务器 ➝ 客户端)
  • 支持断线重连

📦 安装依赖

无特别依赖,只需要一个支持 CORS 的 SSE 后端(如 Node.js + Express + OpenAI API)。


🧩 封装 useSSE Hook

我们封装一个可复用的 useSSE Hook,负责建立连接、接收消息、关闭连接等逻辑:

ini 复制代码
ts
复制编辑
// src/hooks/useSSE.ts
import { useEffect, useRef } from 'react';

interface UseSSEOptions {
  onMessage: (data: string) => void;
  onError?: (error: Event) => void;
  onOpen?: () => void;
}

export function useSSE(url: string, options: UseSSEOptions) {
  const eventSourceRef = useRef<EventSource | null>(null);

  useEffect(() => {
    if (!url) return;

    const eventSource = new EventSource(url);
    eventSourceRef.current = eventSource;

    eventSource.onopen = () => {
      options.onOpen?.();
    };

    eventSource.onmessage = (event) => {
      options.onMessage(event.data);
    };

    eventSource.onerror = (error) => {
      options.onError?.(error);
      eventSource.close();
    };

    return () => {
      eventSource.close();
    };
  }, [url]);
}

💬 实现聊天页面

我们在 Chat.tsx 页面中,调用这个 Hook,构建完整的聊天界面:

typescript 复制代码
tsx
复制编辑
// src/pages/Chat.tsx
import React, { useState } from 'react';
import { useSSE } from '@/hooks/useSSE';

const Chat: React.FC = () => {
  const [messages, setMessages] = useState<string[]>([]);
  const [input, setInput] = useState('');
  const [chatUrl, setChatUrl] = useState<string | null>(null);

  useSSE(chatUrl || '', {
    onMessage: (data) => {
      setMessages((prev) => {
        const newMessages = [...prev];
        if (typeof data === 'string') {
          newMessages[newMessages.length - 1] += data;
        }
        return newMessages;
      });
    },
    onOpen: () => {
      setMessages((prev) => [...prev, '']); // 占位符
    },
    onError: (err) => {
      console.error('SSE error', err);
    },
  });

  const handleSend = () => {
    if (!input.trim()) return;
    setChatUrl(`http://localhost:3000/sse?prompt=${encodeURIComponent(input)}`);
    setInput('');
  };

  return (
    <div style={{ padding: 24 }}>
      <h2>🤖 与 AI 聊天</h2>
      <div
        style={{
          border: '1px solid #ccc',
          height: 300,
          overflowY: 'auto',
          padding: 12,
          marginBottom: 12,
        }}
      >
        {messages.map((msg, idx) => (
          <p key={idx}>{msg}</p>
        ))}
      </div>
      <input
        value={input}
        onChange={(e) => setInput(e.target.value)}
        style={{ width: '80%' }}
        placeholder="请输入你的问题..."
      />
      <button onClick={handleSend} style={{ marginLeft: 8 }}>
        发送
      </button>
    </div>
  );
};

export default Chat;

✅ 测试效果

确保你有一个兼容 SSE 的后端接口,例如:

vbnet 复制代码
vbnet
复制编辑
GET /sse?prompt=你好
Content-Type: text/event-stream

启动项目后,打开聊天页面,输入问题,即可看到 AI 的回答像打字机一样"逐字"输出。


📌 优化建议

  • 增加 loading/loading dot 动画
  • 使用对话上下文管理完整会话
  • 对 SSE 数据格式进一步封装(如多字段 JSON)

📚 参考资料


如果你觉得本文对你有帮助,不妨点个赞、收藏支持一下 ❤️

如有问题或想法,欢迎评论区交流~

相关推荐
薄雾晚晴13 分钟前
大屏开发实战:用 autofit.js 实现 1920*1080 设计稿完美自适应,告别分辨率变形
前端·javascript·vue.js
yannick_liu15 分钟前
vue项目打包后,自动部署到服务器上面
前端
布列瑟农的星空15 分钟前
升级一时爽,降级火葬场——tailwind4降级指北
前端·css
谁黑皮谁肘击谁在连累直升机16 分钟前
for循环的了解与应用
前端·后端
不系舟同学20 分钟前
Three.js + CSS3DSprite 首帧精灵图模糊问题排查、解决
前端
诚实可靠王大锤41 分钟前
react-native集成PDF预览组件react-native-pdf
前端·react native·react.js·pdf
Hilaku1 小时前
前端的设计模式?我觉得90%都是在过度设计!
前端·javascript·设计模式
Miloce1 小时前
零成本搭建跨域代理服务 - Cloudflare Workers实战指南
前端
叫我詹躲躲1 小时前
🌟 回溯算法原来这么简单:10道经典题,一看就明白!
前端·算法·leetcode
薄雾晚晴1 小时前
大屏实战:ECharts 自适应,用 ResizeObserver 解决容器尺寸变化难题
前端·javascript·vue.js