React 最小闭环(第 1-2 周)
目标:用 React 跑通“流式输出 + 取消”的最小闭环。
目录
- 架构图(最小闭环)
- 任务清单
- 最小可运行代码
- 进阶:useStream Hook(建议直接用)
- 项目结构
- 运行步骤(最少流程)
- 验收标准
- 常见坑排查清单
- 详细解析:流式渲染性能
- Mermaid:前端流式状态机
- 调试建议
- 扩展任务
- 组件拆分建议
- 错误处理示例
- useEffect 清理(防止内存泄漏)
- Mock 数据(可测试 UI)
- 扩展练习
- 完整组件示例(最小版)
- 性能优化 Checklist
- 测试用例
- 进阶扩展
- 接口定义(最小)
- UI 交互清单
- 错误码建议
- 附录:练习清单(20 条)
- 额外检查项
- 最小验收截图清单
- 完整可运行代码(React 最小闭环)
架构图(最小闭环)
任务清单
- React 聊天 UI
- SSE 流式输出
- 中断/继续
最小可运行代码
fetch("/api/ai/chat/stream", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: "你好" }),
});
进阶:useStream Hook(建议直接用)
import { useState } from "react";
export function useStream() {
const [text, setText] = useState("");
const [loading, setLoading] = useState(false);
async function start(input: string) {
setText("");
setLoading(true);
const res = await fetch("/api/ai/chat/stream", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: input }),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
setText((t) => t + decoder.decode(value));
}
setLoading(false);
}
return { text, loading, start };
}
节流渲染(推荐)
let buf = "";
let last = 0;
const interval = 80;
function onChunk(chunk: string) {
buf += chunk;
const now = Date.now();
if (now - last > interval) {
setText((t) => t + buf);
buf = "";
last = now;
}
}
项目结构
react-ai-demo/
src/
App.tsx
Chat.tsx
useStream.ts
api.ts
运行步骤(最少流程)
- 启动后端 SSE 服务
- 前端发起流式请求
- 中断按钮触发 AbortController
验收标准
- 流式输出连续不卡顿
- 点击停止立即中断
常见坑排查清单
- 频繁重渲染:每 token setState → 加节流缓冲
- Markdown 卡顿:流式阶段纯文本,结束后再渲染
- 接口跨域:开发期用 Vite proxy 或服务端开启 CORS
详细解析:流式渲染性能
为什么会卡
- 每个 token 都触发 setState
- Markdown 渲染耗时
- 频繁 reflow
推荐策略
- 缓冲 50-100ms 合并更新
- 流式阶段纯文本
- 完成后再渲染 Markdown
Mermaid:前端流式状态机
调试建议
- 打印 TTFT(首 token 时间)
- 打印响应长度
- 记录 abort 触发次数
扩展任务
- 加入“重新生成”按钮
- 加入“复制回答”按钮
- 加入“引用面板”占位组件
组件拆分建议
ChatInput:输入与按钮ChatOutput:流式文本显示useStream:请求与状态
错误处理示例
try {
await start(input);
} catch (e) {
setError("请求失败,请重试");
}
useEffect 清理(防止内存泄漏)
useEffect(() => {
return () => controller.abort();
}, []);
Mock 数据(可测试 UI)
setText("这是模拟输出...
");
扩展练习
- 加“重新生成”按钮
- 加“复制回答”按钮
- 加“耗时统计”展示
完整组件示例(最小版)
function Chat() {
const { text, loading, start } = useStream();
const [input, setInput] = useState("");
return (
<div>
<textarea value={input} onChange={(e)=>setInput(e.target.value)} />
<button onClick={()=>start(input)} disabled={loading}>发送</button>
<pre>{text}</pre>
</div>
);
}
性能优化 Checklist
- 节流渲染
- 关闭 Markdown 实时渲染
- 限制单次输出长度
测试用例
- 发送空输入 → 提示错误
- 发送正常输入 → 流式输出
- 中断 → 立即停止
进阶扩展
- 记录 TTFT
- 记录总耗时
- 展示 traceId
接口定义(最小)
请求:
{ "message": "你好" }
响应(SSE):
data: 你
data: 好
data: [DONE]
UI 交互清单
- 输入区
- 发送按钮
- 停止按钮
- 输出区
错误码建议
E_TIMEOUT:超时E_ABORT:用户中断E_UPSTREAM:上游错误
附录:练习清单(20 条)
- 添加“重新生成”按钮
- 添加“复制回答”按钮
- 添加“清空对话”按钮
- 添加“输入长度限制”
- 添加“加载中”状态
- 添加“错误提示”
- 添加“自动滚动到底部”
- 添加“键盘快捷键”
- 添加“消息时间戳”
- 添加“消息编号”
- 添加“分隔线”
- 添加“空状态提示”
- 添加“示例问题按钮”
- 添加“输出字符计数”
- 添加“请求耗时显示”
- 添加“禁用重复发送”
- 添加“重试按钮”
- 添加“失败兜底文案”
- 添加“debug 面板”
- 添加“traceId 显示”
额外检查项
- 输入为空时不发送
- 发送中禁用按钮
- 停止后可继续发送
最小验收截图清单
- 输入区
- 输出区
- 停止按钮
完整可运行代码(React 最小闭环)
源码目录:
docs/demos/ai-chat-demo
服务端
cd docs/demos/ai-chat-demo/server
npm i
npm run dev
前端
cd docs/demos/ai-chat-demo/web
npm i
npm run dev