一个完整的类 ChatGPT 对话系统,支持流式输出、打断,会话历史,前后端分离架构,非常适合拿来练手熟悉技术实现或者面试使用
基于 React 18 + TypeScript + Vite 的 AI 对话系统前端,支持流式对话、深度思考、会话管理等功能。
技术栈
- React 18 - UI 框架
- TypeScript - 类型安全
- Vite - 构建工具
- Zustand - 状态管理
- Ant Design 5 - UI 组件库
- React Router 6 - 路由管理
- React Markdown - Markdown 渲染
一、打字机效果(流式对话)的实现
实现原理: 使用 SSE (Server-Sent Events) 技术,后端逐字返回,前端实时追加渲染。
核心代码:
typescript
// ChatEditor/index.tsx
const handleSubmit = async () => {
// 1. 创建 AbortController 用于中断请求
abortControllerRef.current = new AbortController();
// 2. 调用后端流式接口
const response = await fetch("/api/chat/stream", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ sessionId, content, history }),
signal: abortControllerRef.current.signal, // 绑定中断信号
});
// 3. 获取 ReadableStream 读取器
const reader = response.body?.getReader();
const decoder = new TextDecoder();
let fullContent = "";
// 4. 循环读取流数据
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 5. 解码二进制数据
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split("\n");
for (const line of lines) {
// 6. 解析 SSE 格式数据
if (line.startsWith("data: ") && line !== "data: [DONE]") {
const data = JSON.parse(line.slice(6));
const delta = data.choices?.[0]?.delta;
// 7. 累加内容并实时更新 UI
if (delta?.content) {
fullContent += delta.content;
updateMessage(sessionId, aiMessageId, fullContent);
}
}
}
}
};
关键点:
response.body.getReader()- 获取流的读取器TextDecoder- 将 Uint8Array 解码为字符串reader.read()- 异步读取下一块数据- SSE 格式解析 - 以
data:开头的行是有效数据
二、中断功能的实现
实现原理: 使用 AbortController 中断 Fetch 请求,同时后端检测连接状态停止生成。
前端实现:
typescript
// ChatEditor/index.tsx
const abortControllerRef = useRef<AbortController | null>(null);
// 开始对话时创建 AbortController
const handleSubmit = async () => {
abortControllerRef.current = new AbortController();
try {
const response = await fetch("/api/chat/stream", {
// ...
signal: abortControllerRef.current.signal, // 绑定信号
});
// ...
} catch (error) {
// 判断是否为中断错误
if (error instanceof Error && error.name === "AbortError") {
console.log("用户主动中断");
isAborted = true;
}
}
};
// 打断按钮点击事件
const handleStop = () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort(); // 触发中断
}
};
后端配合(Java):
java
// AiChatServiceImpl.java
private boolean isClientConnected(HttpServletResponse response, PrintWriter writer) {
try {
writer.write("");
writer.flush();
return !writer.checkError(); // 检测连接是否有效
} catch (Exception e) {
return false;
}
}
// 流式输出时检测连接
while ((line = reader.readLine()) != null) {
if (!isClientConnected(response, writer)) {
System.out.println("客户端已断开,停止生成");
break;
}
writer.write(line + "\n");
writer.flush();
}
关键点:
AbortController- Web 标准 API,用于中断请求signal- 将中断信号绑定到 fetchAbortError- 中断时抛出的特定错误- 后端
checkError()- 检测客户端是否断开
三、Loading 动画(三个跳动的点)
实现原理: 使用 CSS 动画,三个点依次上下跳动,形成律动感。
组件实现:
tsx
// ChatMessage/index.tsx
{
message.isGenerating && !message.content && (
<span className="loading-dots">
<span className="dot">.</span>
<span className="dot">.</span>
<span className="dot">.</span>
</span>
);
}
CSS 动画:
less
.loading-dots {
display: inline-flex;
align-items: center;
gap: 2px;
font-size: 20px;
font-weight: bold;
color: var(--text-secondary);
.dot {
animation: bounce 1.4s infinite ease-in-out both;
// 依次延迟,形成波浪效果
&:nth-child(1) {
animation-delay: -0.32s;
}
&:nth-child(2) {
animation-delay: -0.16s;
}
&:nth-child(3) {
animation-delay: 0s;
}
}
}
@keyframes bounce {
0%,
80%,
100% {
transform: translateY(0);
opacity: 0.5;
}
40% {
transform: translateY(-6px); // 向上跳动
opacity: 1;
}
}
关键点:
animation-delay- 负值让动画提前开始,形成依次跳动ease-in-out- 缓动函数,让动画更自然both- 动画开始前和结束后都应用关键帧样式transform: translateY- 使用 GPU 加速,性能更好
四、状态管理设计
Zustand Store 设计:
typescript
// useSessionStore.ts
interface SessionState {
sessions: Session[]; // 所有会话
currentSessionId: string | null;
loading: boolean;
createSession: () => string;
addMessage: (sessionId, role, content) => string;
updateMessage: (
sessionId,
messageId,
content,
reasoningContent,
isComplete
) => void;
}
export const useSessionStore = create<SessionState>()(
persist(
(set, get) => ({
// 创建会话
createSession: () => {
const id = uuidv4();
const newSession: Session = {
id,
title: "新对话",
messages: [],
model: "deepseek-chat",
};
set((state) => ({
sessions: [newSession, ...state.sessions],
currentSessionId: id,
}));
return id;
},
// 添加消息,AI 消息标记为生成中
addMessage: (sessionId, role, content) => {
const message: Message = {
id: uuidv4(),
role,
content,
isGenerating: role === "assistant" ? true : undefined,
};
// ...
},
// 流式更新,isComplete 为 true 时停止 loading
updateMessage: (
sessionId,
messageId,
content,
reasoningContent,
isComplete = false
) => {
set((state) => ({
sessions: state.sessions.map((s) =>
s.id === sessionId
? {
...s,
messages: s.messages.map((m) =>
m.id === messageId
? {
...m,
content,
reasoningContent,
isGenerating: !isComplete,
}
: m
),
}
: s
),
}));
},
}),
{
name: "session-storage", // localStorage 持久化
storage: createJSONStorage(() => localStorage),
}
)
);
关键点:
persist中间件 - 自动持久化到 localStorageisGenerating- 控制 Loading 动画的显示- 函数式更新
set((state) => ...)- 基于最新状态更新 - 不可变数据 - 使用展开运算符创建新对象
五、防止重复创建会话
问题: React 18 StrictMode 会故意双重调用某些函数来检测副作用,导致 useEffect 执行两次,创建两个会话。
解决方案:
typescript
// Chat/index.tsx
const Chat: React.FC = () => {
const { sessionId } = useParams();
const hasCreatedSession = useRef(false); // 使用 ref 标记
useEffect(() => {
// 只有未创建过且没有 sessionId 时才创建
if (!sessionId && !hasCreatedSession.current) {
hasCreatedSession.current = true; // 标记已创建
const newId = createSession();
navigate(`/chat/${newId}`, { replace: true });
}
}, [sessionId]);
};
关键点:
useRef- 在组件生命周期内保持值不变hasCreatedSession.current- 标记是否已执行replace: true- 替换当前历史记录,避免返回时重复触发
项目结构
bash
src/
├── api/
│ ├── backend.ts # 后端 API 封装
│ └── deepseek.ts # (已废弃)
├── components/
│ ├── ChatEditor/ # 对话输入(流式请求)
│ ├── ChatMessage/ # 消息气泡(Loading 动画)
│ ├── SessionList/ # 会话列表
│ └── Sidebar/ # 侧边栏
├── pages/
│ ├── Chat/ # 对话页(StrictMode 处理)
│ ├── Login/ # 登录注册
│ └── Setting/ # 设置页
├── stores/
│ └── useSessionStore.ts # Zustand 状态管理
└── types/
└── chat.ts # TypeScript 类型定义
启动方式
bash
npm install
npm run dev
核心设计亮点
| 功能 | 技术方案 | 关键点 |
|---|---|---|
| 打字机效果 | SSE + ReadableStream | 逐块读取、实时解码 |
| 中断功能 | AbortController | 前后端配合检测连接 |
| Loading 动画 | CSS Animation | animation-delay 波浪效果 |
| 状态管理 | Zustand + Persist | localStorage 自动持久化 |
| StrictMode 处理 | useRef 标记 | 防止副作用重复执行 |
下一篇分享后端:源码地址