把 270 行 Generator 拆成通用 Hook:我封装了一个 useStreamChat
背景:从组件到 Hook 的迁移项目里的 AI 对话核心逻辑全部写在一个Generator组件里——270 行代码,包含了消息管理、流式请求、状态控制、键盘事件、滚动行为。功能没问题,但有两个痛点:不可复用:如果要新建一个"AI 代码审查"的面板,不能直接引用这套逻辑——得复制一份 Generator 然后改。测试困难:组件里的逻辑和 UI 混在一起,要测试"发送消息后 loading 是否变为 true"必须挂载整个组件。我参考了这个 Demo 的精髓(双状态模型、ReadableStream、AbortController),把核心逻辑抽成一个190 行的通用 Hook——useStreamChat。这篇文章完整拆解它的设计和实现。整体架构┌─────────────────────────────────────┐ │ 业务组件层 │ │ (AI 助手面板 / 代码审查 / 翻译助手) │ └──────────────┬──────────────────────┘ │ 只消费返回值 ┌──────────────▼──────────────────────┐ │ useStreamChat Hook │ │ ┌────────────────────────────────┐ │ │ │ 状态管理 │ │ │ │ messageList / currentMessage │ │ │ │ / loading / error │ │ │ ├────────────────────────────────┤ │ │ │ 方法 │ │ │ │ send / stop / retry / clear │ │ │ ├────────────────────────────────┤ │ │ │ 内部流程 │ │ │ │ fetch SS → ReadableStream │ │ │ │ → 逐字解码 → 信号控制 │ │ │ └────────────────────────────────┘ │ └──────────────┬──────────────────────┘ │ 只调 API ┌──────────────▼──────────────────────┐ │ 后端 API │ │ POST /api/generate │ └─────────────────────────────────────┘Hook 只暴露状态和方法,不关心 UI。组件只关心"把状态渲染成什么样子",不关心流是怎么读的。实现细节第一步:定义 Hook 的入参和出参// hooks/useStreamChat.tsinterfaceUseStreamChatOptions{apiEndpoint:string;// API 地址maxHistoryMessages?:number;// 最大上下文消息数,默认 9systemPrompt?:string;// 系统角色设定temperature?:number;// 模型温度,默认 0.6onError?:(error:Error)=void;// 错误回调}interfaceUseStreamChatReturn{// 状态(只读)messages:ChatMessage[];// 完整的消息列表(含流式中的)currentStreaming:string;// 当前正在流式接收中的文本isLoading:boolean;// 是否正在请求中error:ChatError|null;// 当前错误// 操作方法send:(content:string)=Promisevoid;// 发送消息stop:()=void;// 停止流式请求retry:()=void;// 重试最后一条clear:()=void;// 清空对话}明确区分"状态"和"操作"——状态只读、操作只写。没有 setter 暴露给外部,防止组件误改内部状态。第二步:实现双状态模型这是从原项目中抽象出来的核心设计:// hooks/useStreamChat.tsfunctionuseStreamChat(options:UseStreamChatOptions):UseStreamChatReturn{const{apiEndpoint,maxHistoryMessages=9,systemPrompt="",temperature=0.6,onError,}=options;// ===== 状态 =====const[messageList,setMessageList]=createSignalC