跳到主要内容

React 最小闭环(第 1-2 周)

目标:用 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

运行步骤(最少流程)

  1. 启动后端 SSE 服务
  2. 前端发起流式请求
  3. 中断按钮触发 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 条)

  1. 添加“重新生成”按钮
  2. 添加“复制回答”按钮
  3. 添加“清空对话”按钮
  4. 添加“输入长度限制”
  5. 添加“加载中”状态
  6. 添加“错误提示”
  7. 添加“自动滚动到底部”
  8. 添加“键盘快捷键”
  9. 添加“消息时间戳”
  10. 添加“消息编号”
  11. 添加“分隔线”
  12. 添加“空状态提示”
  13. 添加“示例问题按钮”
  14. 添加“输出字符计数”
  15. 添加“请求耗时显示”
  16. 添加“禁用重复发送”
  17. 添加“重试按钮”
  18. 添加“失败兜底文案”
  19. 添加“debug 面板”
  20. 添加“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