在 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)

📚 参考资料


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

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

相关推荐
沙漠之主11 小时前
C++编程教学设计资料:从入门到实战的完整课程方案
java·前端·c++
hasty11 小时前
不上传新包,也能改变用户拿到的版本:npm dist-tag 的 OIDC 权限治理
前端·npm·node.js
Csvn12 小时前
diff 算法(虚拟 DOM Reconciliation)
前端
前端snow13 小时前
ai agent --- 文件存储
前端
Frag0ut13 小时前
Chrome四大版本获取及共存指南:Stable/Beta/Dev/Canary
前端·chrome·浏览器·dev·beta·canary·共存版
IT_陈寒14 小时前
SpringBoot自动配置坑了我一把,原来是这样绕过去的
前端·人工智能·后端
广州华水科技14 小时前
单北斗GNSS变形监测在大坝安全监测中的应用与优势
前端
zeng不错15 小时前
B站 173 个投稿活动看到眼瞎?我写了个扩展,主打一个精准打击
前端
Latchh15 小时前
前端导出的JPEG红字发糊,质量要拉到100才清楚
前端·图像处理·人工智能·计算机视觉
默_笙15 小时前
🚦 请三个 AI 考官给 RAG 打分:量化评估实战(下)
前端·javascript