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

📚 参考资料


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

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

相关推荐
雨汨8 分钟前
el-input限制输入数字,输入中文后数字校验失效
前端·javascript·vue.js
保持学习ing14 分钟前
帝可得- 人员管理
前端·vue.js·elementui
難釋懷15 分钟前
Vue全局事件总线
前端·javascript·vue.js
独立开阀者_FwtCoder29 分钟前
使用这个新的 ECMAScript 运算符告别 Try/Catch!
前端·javascript·github
云浪29 分钟前
让元素舞动!深度解密 CSS 旋转函数
前端·css
cdcdhj30 分钟前
vue中events选项与$on监听自定义事件他们的区别与不同,以及$emit与$on之间通信和mounted生命周期钩子函数有哪些作用和属性
前端·javascript·vue.js
Jinxiansen021142 分钟前
Vue 3 弹出式计算器组件(源码 + 教程)
前端·javascript·vue.js
东京老树根1 小时前
SAP学习笔记 - 开发22 - 前端Fiori开发 数据绑定(Jason),Data Types(数据类型)
前端·笔记·学习
前端 贾公子1 小时前
手写 vue 源码 === runtime-dom 实现
前端·javascript·css
江城开朗的豌豆1 小时前
JavaScript篇:typeof 的魔法:原来你是这样判断类型的!
前端·javascript·面试