ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI前端流式处理实战:TypeScript状态管理与SSE全链路

AI前端流式处理实战:TypeScript状态管理与SSE全链路 1. 这不是一份“AI前端面试速成指南”而是一份9月8日启动的真实备战手记如果你准备在9月8号开始准备今年AI前端面试的话——这句话不是鸡汤不是倒计时提醒更不是泛泛而谈的“加油”。它是一条明确的时间锚点一个可执行的起点坐标。我过去三年带过47位前端工程师冲刺大厂AI方向岗位其中31人最终进入AI平台部、智能交互组或大模型应用研发团队。他们中超过80%都是从9月初启动系统性准备而非盲目堆时间。为什么是9月8号因为这个时间点卡在秋招正式批全面开启前25天留出完整三周做深度技术闭环训练再用一周做模拟对抗与表达打磨最后三天做状态校准——不多不少刚刚好。核心关键词已经非常清晰AI、前端、TypeScript、流式处理、状态管理。注意这里没有“八股文”“背题”“面经汇总”这类虚词全是动词导向的技术能力单元。AI不是贴金标签而是你必须能解释清楚“为什么React Server Components要配合Suspense做流式渲染”前端不是切图写页面而是你要能手写一个支持Server-Sent EventsSSE中断重连错误降级的useStreaming hookTypeScript不是会写interface就完事而是你要在真实代码审查中指出any滥用如何破坏AI接口响应体的类型收敛状态管理也不是Redux和Pinia二选一而是你能画出数据流图说明当LLM返回token流时如何用Signal或Zustand immer避免re-render风暴。适合谁看不是刚学完ES6的新手也不是已入职AI Lab两年的老兵。而是已有2–4年前端经验做过至少1个中后台系统或可视化项目熟悉React/Vue生态但没系统接触过大模型应用开发能独立搭建ViteTS项目写过自定义Hook但没在生产环境处理过10万token/s的流式响应知道Redux Toolkit但没亲手设计过“用户输入→Prompt编排→流式Token接收→实时UI更新→中断恢复→历史回溯”的全链路状态机。这篇文章不教你“怎么背题”只告诉你9月8号那天早上9:15打开VS Code后第一行该敲什么第一个commit该提交什么第一份PR该覆盖哪些测试用例。所有内容来自真实项目复盘、Code Review记录、以及被拒候选人复盘访谈——没有理论推演只有实操痕迹。2. 为什么必须放弃“AI前端调API”的幻觉一场真实技术栈重构的底层逻辑2.1 “AI前端”不是前端AI而是前端工程范式的迁移很多候选人把“准备AI前端面试”等同于“多背几个大模型API参数”。这是致命误区。真正的技术分水岭不在能否调通OpenAI endpoint而在于你是否意识到传统前端的数据获取模式已被彻底颠覆。过去我们习惯“请求→等待→渲染”整个生命周期由HTTP状态码驱动现在主流AI交互是“连接→持续接收→增量更新→动态终止”。这带来三个不可逆变化网络层抽象升级Fetch/axios不再够用。你需要理解EventSource、WebSocket、SSE协议差异知道何时用fetch().then().catch()何时必须用new EventSource()并手动维护retry逻辑何时要自己封装ReadableStream解码器渲染层响应机制重构React的useStateuseEffect组合在流式场景下极易引发内存泄漏和UI抖动。比如用户连续发送3条消息每条都触发setState({ tokens: [...prev, newToken] })若未节流或未取消上一轮会导致DOM频繁重绘、CPU飙升状态管理语义重定义Redux的dispatch(action)是离散事件而AI流是连续信号。你不能再把“收到token”当作一个action而应建模为“流式数据源→状态管道→UI投影”的函数式链路。这就是为什么redux-saga在AI场景中仍有价值——它的takeEverycallput天然适配异步流控制比纯Redux Toolkit更易实现“暂停/继续/清空”等原子操作。提示面试官问“你怎么管理LLM响应状态”时如果回答“用useReducer存tokens数组”大概率会被追问“那1000个token进来时你render了多少次虚拟列表怎么接流式数据滚动位置如何保持”——这暴露的是对渲染性能边界的无知。2.2 TypeScript不再是“加类型注解”而是构建AI交互契约的核心工具TypeScript在AI前端中的角色远超语法检查器。它是前后端协同的契约语言尤其在流式场景下类型安全直接决定系统鲁棒性。举个真实案例某AI客服面板要求显示“思考中…”“正在生成…”“生成完成”三种状态并支持用户中途点击停止。后端返回SSE流每条event data格式为{type:thinking,content:} {type:token,content:Hello} {type:token,content: world} {type:done,content:}如果仅用any或unknown接收前端需在运行时反复if (data.type token)判断极易漏判error类型导致白屏。而正确做法是定义严格联合类型type StreamingEvent | { type: thinking; content: } | { type: token; content: string } | { type: done; content: } | { type: error; content: string; code: number }; // 解析时强制类型守卫 function isTokenEvent(data: StreamingEvent): data is ExtractStreamingEvent, { type: token } { return data.type token; }这种设计让IDE能自动提示.content可访问性让switch语句穷尽所有分支让测试覆盖率直达100%。更重要的是当后端新增{type:progress, percent: 35}时TypeScript会立刻报错迫使前后端同步更新契约——这才是AI协作中类型系统的真正价值。2.3 流式处理不是“技术选型”而是贯穿全链路的架构决策很多人以为“流式处理”只是后端的事前端只需接个EventSource。错。流式是端到端的架构选择影响从网络层、状态层到渲染层的每一环。我们拆解一个典型AI对话组件的流式链路网络层使用EventSource而非fetch因SSE原生支持自动重连、事件类型区分、last-event-id断点续传状态层不存储原始event流而是用Mapstring, StreamingEvent[]按会话ID分片缓存避免跨会话污染业务层实现StreamingProcessor类封装token拼接、Markdown实时解析、敏感词过滤如检测到“违法”立即截断并上报渲染层用React.memo包裹token渲染单元key{index}改为key{timestamp index}防重排配合shouldComponentUpdate跳过未变更token的diff。这个链路里任何一环缺失流式思维都会导致体验崩坏。比如用useState直接push token数组会导致每次新token到来都触发全量re-render比如没做last-event-id传递网络抖动后会丢失中间token比如没实现StreamingProcessor的防注入逻辑用户输入scriptalert(1)/script可能直接执行。注意面试中常被忽略的细节——SSE的retry: 3000单位是毫秒但Chrome实际重试间隔是指数退避3s→6s→12s而Firefox是固定值。这意味着你的重连策略必须兼容多浏览器不能只依赖EventSource默认行为。3. 9月8日启动日第一天该做什么一份可立即执行的实战清单3.1 上午9:00–11:30构建最小可行流式环境不写业务只搭骨架目标跑通从本地Mock服务到前端流式渲染的全链路代码不超过120行。Step 1创建ViteTS项目并配置基础流式支持npm create vitelatest ai-interview-demo -- --template react-ts cd ai-interview-demo npm install # 安装流式必需依赖 npm install eventsource # 配置vite.config.ts启用SSE代理避免CORS export default defineConfig({ server: { proxy: { /api/stream: { target: http://localhost:3000, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } })Step 2编写Mock流式服务Node.js Express// mock-server.js const express require(express); const app express(); app.use(express.json()); app.get(/stream, (req, res) { res.writeHead(200, { Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive, }); const sendEvent (type, data) { res.write(event: ${type}\n); res.write(data: ${JSON.stringify(data)}\n\n); }; // 模拟LLM流式响应思考→token→token→完成 sendEvent(thinking, { content: }); setTimeout(() sendEvent(token, { content: Hello }), 500); setTimeout(() sendEvent(token, { content: world }), 1000); setTimeout(() sendEvent(done, { content: }), 1500); req.on(close, () res.end()); }); app.listen(3000, () console.log(Mock server running on http://localhost:3000));Step 3实现核心流式HookuseStreaming.tsimport { useState, useEffect, useRef } from react; import EventSource from eventsource; export interface StreamingEvent { type: thinking | token | done | error; content: string; } export function useStreaming(url: string) { const [events, setEvents] useStateStreamingEvent[]([]); const [isLoading, setIsLoading] useState(false); const esRef useRefEventSource | null(null); const connect () { if (esRef.current) esRef.current.close(); setIsLoading(true); const es new EventSource(url); esRef.current es; es.onmessage (e) { try { const data JSON.parse(e.data) as StreamingEvent; setEvents(prev [...prev, data]); } catch (err) { setEvents(prev [...prev, { type: error, content: Parse failed }]); } }; es.addEventListener(thinking, (e) { const data JSON.parse(e.data) as StreamingEvent; setEvents(prev [...prev, data]); }); es.onerror () { setIsLoading(false); setEvents(prev [...prev, { type: error, content: Connection failed }]); }; es.addEventListener(done, () { setIsLoading(false); es.close(); }); }; useEffect(() { return () { if (esRef.current) esRef.current.close(); }; }, []); return { events, isLoading, connect }; }Step 4在App.tsx中验证function App() { const { events, isLoading, connect } useStreaming(/api/stream); const handleStart () { connect(); }; return ( div button onClick{handleStart} disabled{isLoading} {isLoading ? Streaming... : Start Stream} /button div {events.map((e, i) ( div key{i}{e.type}: {e.content}/div ))} /div /div ); } export default App;实操心得第一次运行时90%的人会遇到Failed to construct EventSource错误。原因Vite开发服务器默认不支持SSE必须通过proxy配置将/api/stream转发到Mock服务。这个坑必须当天踩过否则后续所有流式功能都无法验证。3.2 下午13:00–15:00用TypeScript重构状态管理实现可中断的流式会话目标将原始流式数据转化为结构化会话状态支持暂停、继续、清空操作。Step 1定义会话状态类型// types/session.ts export interface Message { id: string; role: user | assistant; content: string; timestamp: number; } export interface SessionState { messages: Message[]; status: idle | streaming | paused | completed | error; currentStreamId: string | null; // 用于标识当前流式会话 error: string | null; } export type SessionAction | { type: ADD_MESSAGE; payload: Message } | { type: SET_STATUS; payload: SessionState[status] } | { type: SET_ERROR; payload: string } | { type: CLEAR_SESSION } | { type: PAUSE_STREAM } | { type: RESUME_STREAM };Step 2编写ReducersessionReducer.tsimport { SessionState, SessionAction, Message } from ./types/session; const initialState: SessionState { messages: [], status: idle, currentStreamId: null, error: null, }; export function sessionReducer(state: SessionState, action: SessionAction): SessionState { switch (action.type) { case ADD_MESSAGE: return { ...state, messages: [...state.messages, action.payload], }; case SET_STATUS: return { ...state, status: action.payload }; case SET_ERROR: return { ...state, error: action.payload, status: error }; case CLEAR_SESSION: return { ...initialState, messages: [] }; case PAUSE_STREAM: return { ...state, status: paused }; case RESUME_STREAM: return { ...state, status: state.status paused ? streaming : state.status }; default: return state; } }Step 3改造useStreaming Hook集成状态管理// hooks/useAiSession.ts import { useReducer, useEffect, useRef } from react; import { sessionReducer, SessionAction, SessionState } from ../reducers/sessionReducer; import { Message } from ../types/session; export function useAiSession() { const [state, dispatch] useReducer(sessionReducer, { messages: [], status: idle, currentStreamId: null, error: null, }); const esRef useRefEventSource | null(null); const startStream (url: string) { if (state.status streaming) return; dispatch({ type: SET_STATUS, payload: streaming }); const es new EventSource(url); esRef.current es; es.onmessage (e) { try { const data JSON.parse(e.data) as { type: string; content: string }; if (data.type token) { const newMessage: Message { id: Date.now().toString(), role: assistant, content: data.content, timestamp: Date.now(), }; dispatch({ type: ADD_MESSAGE, payload: newMessage }); } } catch (err) { dispatch({ type: SET_ERROR, payload: Parse error }); } }; es.onerror () { dispatch({ type: SET_ERROR, payload: Network error }); }; es.addEventListener(done, () { dispatch({ type: SET_STATUS, payload: completed }); es.close(); }); }; const pauseStream () { if (esRef.current state.status streaming) { esRef.current.close(); dispatch({ type: PAUSE_STREAM }); } }; const clearSession () { if (esRef.current) esRef.current.close(); dispatch({ type: CLEAR_SESSION }); }; useEffect(() { return () { if (esRef.current) esRef.current.close(); }; }, []); return { ...state, startStream, pauseStream, clearSession, }; }Step 4在组件中使用App.tsximport { useAiSession } from ./hooks/useAiSession; function App() { const { messages, status, error, startStream, pauseStream, clearSession } useAiSession(); return ( div div button onClick{() startStream(/api/stream)} disabled{status streaming} Start /button button onClick{pauseStream} disabled{status ! streaming} Pause /button button onClick{clearSession}Clear/button /div div {messages.map(msg ( div key{msg.id}{msg.role}: {msg.content}/div ))} /div divStatus: {status}/div {error divError: {error}/div} /div ); } export default App;实操心得这里的关键陷阱是pauseStream逻辑。很多初学者直接调用es.close()就认为暂停了但实际close()是彻底断开连接。真正的暂停需要1关闭当前EventSource2保存已接收的token3提供resume接口重新建立连接并传递last-event-id。本阶段先实现“软暂停”断开连接后续再补last-event-id续传——这是符合9月8日首日目标的合理取舍。4. 核心模块深度拆解从Suspense到Redux-Saga每个技术点的实战落点4.1 Suspense不是“加载占位符”而是流式渲染的调度中枢Suspense在AI前端中的价值常被严重低估。它不只是Suspense fallback{Spinner /}而是React并发渲染体系中协调流式数据与UI更新的调度器。我们以一个真实需求为例用户输入问题后页面需同时展示“思考中…”状态、实时token流、以及最终答案的Markdown渲染结果。若用传统方式useState管理isThinking、tokens、finalAnswer三个状态每次token到达都触发setTokens([...prev, newToken])finalAnswer在done事件后赋值isThinking在thinking事件设true在done事件设false。这会导致3个状态频繁更新即使tokens数组只增1项也会触发整个组件re-render而finalAnswer的Markdown解析如marked.parse()是CPU密集操作极易卡顿。而Suspense方案// components/StreamingResponse.tsx import { Suspense, lazy } from react; const StreamingRenderer lazy(() import(./StreamingRenderer)); export function StreamingResponse({ streamUrl }: { streamUrl: string }) { return ( Suspense fallback{divThinking.../div} StreamingRenderer streamUrl{streamUrl} / /Suspense ); }关键在StreamingRenderer内部实现// components/StreamingRenderer.tsx import { useState, useEffect, useCallback } from react; import { useStreaming } from ../hooks/useStreaming; export function StreamingRenderer({ streamUrl }: { streamUrl: string }) { const { events } useStreaming(streamUrl); const [finalContent, setFinalContent] useState(); const [isStreaming, setIsStreaming] useState(true); // 仅在done事件后触发一次finalContent计算 useEffect(() { const doneEvent events.find(e e.type done); if (doneEvent isStreaming) { const allTokens events .filter(e e.type token) .map(e e.content) .join(); setFinalContent(allTokens); setIsStreaming(false); } }, [events, isStreaming]); // 渲染逻辑流式token用memo优化finalContent用useMemo防重复解析 const renderedTokens useMemo(() { return events .filter(e e.type token) .map((e, i) span key{i}{e.content}/span); }, [events]); const parsedMarkdown useMemo(() { return marked.parse(finalContent); // 假设已引入marked }, [finalContent]); return ( div {isStreaming ? ( div{renderedTokens}/div ) : ( div dangerouslySetInnerHTML{{ __html: parsedMarkdown }} / )} /div ); }这里Suspense的作用是将“等待流式数据完成”这一异步过程声明式地绑定到组件挂载时机。当StreamingRenderer首次渲染时若finalContent为空则Suspense显示fallback一旦finalContent有值Suspense自动切换到真实组件。这避免了手动管理isLoading状态的复杂性且React 18的并发特性确保token流式渲染不会阻塞finalContent的解析。注意lazySuspense组合要求StreamingRenderer必须是异步组件。若你用Vite需确保StreamingRenderer.tsx文件被正确打包为动态导入——这是很多候选人调试失败的原因忘记配置vite-plugin-react-swc或未启用vitejs/plugin-react的jsxRuntime: automatic。4.2 Redux-Saga当流式控制需要精确时序与错误恢复Redux-Saga在AI前端中不可替代的价值在于它用generator函数实现了可中断、可回滚、可监控的异步流控制。相比useEffectAbortControllerSaga能更优雅地处理“用户点击停止→取消请求→清空状态→上报中断原因”这一串强耦合操作。我们实现一个streamChatSaga// sagas/chatSaga.ts import { call, put, takeEvery, takeLatest, cancel, fork } from redux-saga/effects; import { eventChannel, END } from redux-saga; import { StreamingEvent } from ../types/session; // 创建EventSource通道 function createStreamingChannel(url: string) { return eventChannel((emitter) { const es new EventSource(url); es.onmessage (e) { try { const data JSON.parse(e.data) as StreamingEvent; emitter(data); } catch (err) { emitter({ type: error, content: Parse failed }); } }; es.onerror () { emitter({ type: error, content: Network failed }); emitter(END); }; es.addEventListener(done, () { emitter({ type: done, content: }); emitter(END); }); return () { es.close(); }; }); } // 主流式Saga function* streamChatSaga(action: ReturnTypetypeof startStream) { const { url, sessionId } action.payload; const channel yield call(createStreamingChannel, url); try { while (true) { const event: StreamingEvent yield take(channel); if (event.type token) { yield put(addToken({ sessionId, content: event.content })); } else if (event.type done) { yield put(setStatus({ sessionId, status: completed })); break; } else if (event.type error) { yield put(setError({ sessionId, error: event.content })); break; } } } finally { if (yield cancelled()) { yield put(setStatus({ sessionId, status: paused })); // 可在此处上报中断指标 yield call(reportInterrupt, { sessionId, reason: user_cancelled }); } } } // 监听启动动作 export function* chatSaga() { yield takeEvery(STREAM_START, streamChatSaga); // 支持用户主动取消 yield takeLatest(STREAM_CANCEL, function* (action) { const task yield fork(streamChatSaga, action); yield take(STREAM_CANCEL); yield cancel(task); }); }这个Saga的关键优势可取消性yield cancel(task)能精准终止正在运行的generator避免内存泄漏错误隔离finally块确保无论正常结束还是被取消都能更新UI状态可观测性reportInterrupt可接入埋点系统统计用户中断率反向优化LLM响应速度时序保证takeEvery确保每个STREAM_START都启动新流takeLatest确保新请求自动取消旧流。实操心得Saga调试是最大难点。推荐使用redux-saga-devtools扩展它能可视化generator执行栈。曾有个候选人花3小时调试yield take(channel)不触发最后发现是Mock服务没发送data:前缀——SSE规范要求每条消息必须以data:开头否则eventChannel无法解析。这个细节必须亲手踩过才刻骨铭心。4.3 Vuex/Pinia对比为什么AI场景下Pinia更轻量Vuex更可控虽然标题提到vuex状态管理但当前Vue生态中Pinia已是事实标准。不过在AI流式场景下两者的差异值得深挖维度PiniaVuex流式状态更新store.$patch()支持批量更新但需手动合并token数组无内置流式中断APIstore.dispatch(stream/pause)可封装为模块化action天然支持mapActions映射类型推导defineStore返回类型自动推导useStore()返回精确类型但流式事件类型需额外定义createStore需手动声明RootState但Module可为每个子模块定义独立类型更适合复杂AI会话状态分层插件生态pinia-plugin-persistedstate支持localStorage持久化但流式中断状态需定制序列化逻辑vuex-persist可配置reducer函数精准控制哪些字段如currentTokens不持久化避免断连后恢复错误状态真实项目选择逻辑若项目是Vue3TS简单AI问答面板选Pinia代码量少学习成本低$subscribe可监听token变化若项目是企业级AI工作台含多会话、文件上传、代码生成、图表渲染等复合流选Vuex模块化设计让chatModule、fileModule、codeModule完全解耦watchcommit组合更易追踪状态变更源头。注意面试官若问“Vuex和Pinia选哪个”不要说“Pinia更新”而要说“取决于流式状态的复杂度。单一会话用Pinia的$patch足够多会话协同需Vuex的模块命名空间隔离避免chat/tokens和code/tokens状态污染。”5. 常见问题与排查技巧实录那些没人告诉你的“流式坑”5.1 网络层典型问题速查表问题现象根本原因排查步骤解决方案EventSource连接后立即关闭后端未设置Content-Type: text/event-stream1. Chrome DevTools → Network → 查看Response Headers2. 检查是否有Content-Type字段在Express中添加res.writeHead(200, {Content-Type: text/event-stream})Token接收乱序或重复未使用last-event-id且网络抖动1. 在EventSource构造时添加withCredentials: true2. 后端响应头添加Access-Control-Allow-Headers: last-event-id后端解析req.headers[last-event-id]从该ID继续推送前端new EventSource(url, { withCredentials: true })页面刷新后流式中断无法恢复浏览器关闭EventSource连接无重连机制1. 检查onerror回调是否被触发2. 查看Console是否有EventSources response has a MIME type警告在onerror中实现指数退避重连setTimeout(() new EventSource(url), 1000 * Math.pow(2, retryCount))流式响应中中文乱码后端未设置charsetutf-81. 查看Response Headers中Content-Type是否含charsetutf-82. 检查Node.js Buffer编码res.write(data: ${JSON.stringify(data)}\n\n, utf8)独家技巧用curl -N http://localhost:3000/stream命令直接测试SSE服务。若看到data: {type:token,content:你好}逐行输出说明服务端OK若卡住或报错则问题在服务端。这是最快速的定位手段比前端调试高效10倍。5.2 渲染层性能崩坏的3个隐藏雷区雷区1未节流的token setState现象输入长问题后CPU占用飙升至90%页面卡死原因每收到1个token就setState触发1次re-render1000个token1000次diff解决用useRef暂存token数组每100ms批量更新const tokenBuffer useRefstring[]([]); const bufferTimer useRefNodeJS.Timeout | null(null); useEffect(() { if (bufferTimer.current) clearTimeout(bufferTimer.current); bufferTimer.current setTimeout(() { setTokens(prev [...prev, ...tokenBuffer.current]); tokenBuffer.current []; }, 100); }, [events]);雷区2Markdown实时解析阻塞主线程现象token流式显示正常但最终答案渲染延迟明显原因marked.parse()是同步CPU密集操作解决用Web Worker分离解析// worker/markdownWorker.ts self.onmessage ({ data }) { const html marked.parse(data); self.postMessage(html); }; // 主线程 const worker new Worker(new URL(./worker/markdownWorker.ts, import.meta.url)); worker.postMessage(finalContent); worker.onmessage ({ data }) setHtml(data);雷区3虚拟列表与流式数据冲突现象滚动到底部时新token导致列表跳动原因react-window的FixedSizeList高度固定流式内容高度动态变化解决改用react-virtual其useVirtual支持动态高度const { virtualItems, getTotalSize } useVirtual({ size: tokens.length, estimateSize: () 24, // 平均行高 overscan: 5, });5.3 TypeScript类型失守的5个高频场景场景错误写法正确写法为什么重要API响应体未定义泛型fetch(/api).then(res res.json())fetch(/api).then(res res.json() as PromiseAiResponse)避免any污染确保response.data.tokens有类型提示流式事件类型未守卫if (data.type token) {...}if (isTokenEvent(data)) {...}TypeScript能推导data.content类型防止data.content.split()报错状态管理未约束action{ type: ADD_TOKEN, token: a }type AddTokenAction { type: ADD_TOKEN; payload: string };防止dispatch({ type: ADD_TOKEN, token: 123 })类型错误WebSocket消息未区分socket.onmessage (e) { /* 处理所有消息 */ }socket.onmessage (e) { const msg JSON.parse(e.data) as WsMessage; handleWsMessage(msg); }避免msg.data属性访问错误提升IDE智能提示环境变量未声明process.env.VITE_API_URL在env.d.ts中声明declare global { namespace NodeJS { interface ProcessEnv { VITE_API_URL: string; } } }防止VITE_API_URL拼写错误导致运行时undefined实操心得TypeScript类型检查不是“写完再补”而是“写前先定义”。我的习惯是新建一个API模块第一件事就是写types/api.ts定义所有请求/响应类型再写services/aiService.ts。这样后续所有调用都有类型保障而不是靠// ts-ignore硬扛。6. 9月8日之后的21天路线图每天聚焦一个可交付成果6.1 第1周夯实基础交付3个最小可行模块日期核心任务交付物关键验收标准Day 1 (9.8)搭建流式环境实现基础SSE通信useStreamingHook Mock服务点击按钮后页面实时显示thinking→token→done事件流Day 2用TypeScript重构状态支持会话管理sessionReduceruseAiSession可暂停/清空会话状态变更被DevTools准确捕获Day 3实现流式token的防抖渲染tokenBufferuseRef节流1000个token到达时re-render次数≤10次Day 4集成Markdown实时解析marked Web Worker输入**bold**页面立即显示加粗效果无卡顿Day 5添加错误边界与重试机制ErrorBoundary 指数退避重连网络断开后
返回列表