十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

AI前端实战:SSE流式处理与TypeScript类型安全设计

AI前端实战:SSE流式处理与TypeScript类型安全设计 1. 这不是一份“复习计划”而是一份9月8日启动的AI前端面试实战推演手册如果你准备在9月8号开始准备今年AI前端面试的话——这句话乍看像一句时间提醒实则是一道隐含多重技术坐标的信号弹。它背后锚定的不是传统前端八股文而是2024—2025年真实招聘现场正在发生的结构性迁移AI能力已从“加分项”变为“准入门槛”而前端工程师的战场正从DOM操作层快速下沉到模型交互层、流式协议层与智能协同层。我带过37个前端候选人冲刺大厂AI方向岗其中21人卡在同一个断点他们能手写React Fiber调度算法却说不清为什么EventSource在SSE流中必须配合retry: 3000他们熟背TypeScript泛型约束却在实现AIChatSessionAgentConfig时因类型收敛失败导致整个对话状态不可推导他们知道LLM输出是token流但没亲手处理过stream disconnected before completion: idle timeout waiting for sse这种报错背后的TCP连接复用逻辑。这本手册不教你“怎么背题”而是带你回到9月8日零点——以一个真实项目为切口从第一行代码开始重建你对AI前端技术栈的认知坐标系。核心关键词AI、前端、TypeScript、SSE、流式处理不是并列关系而是存在强依赖链AI提供语义能力 → 前端构建人机界面 → TypeScript保障类型安全 → SSE承载实时流 → 流式处理决定体验上限。比如当面试官问“如何实现一个支持中断重试的AI聊天框”答案绝不是贴一段fetch代码而是要讲清SSE连接生命周期如何与React组件挂载/卸载同步、TypeScript如何用const enum定义流事件类型、idle timeout触发后如何基于Last-Event-ID做断点续传、以及为什么before completion: idle timeout waiting for sse错误本质是服务端未正确设置keep-alive头而非前端代码缺陷。适合谁读三类人必须细看一是已掌握Vue/React但从未接入过真实AI服务的中级前端二是能写Node.js后端却对前端流式协议细节模糊的全栈开发者三是正在规划学习路径、想避开“学了一堆AI概念却写不出可交付组件”的自学者。接下来所有内容都来自我们团队过去14个月在6个AI原生应用智能文档助手、低代码AI表单生成器、实时代码解释器、多模态会议纪要系统、AI测试用例生成平台、前端工程知识图谱中踩出的实操路径。没有理论铺陈只有可验证、可调试、可上线的硬核细节。2. 整体设计思路为什么必须以SSE为锚点重构AI前端架构2.1 拒绝“伪流式”为什么WebSocket不是AI前端的最优解很多候选人一提流式输出就条件反射写WebSocket这是2023年遗留的认知惯性。真实业务场景中SSE在AI前端落地中的综合优势远超WebSocket原因有三第一协议开销与容错性差异。WebSocket需完整握手HTTP Upgrade、维护双工通道、处理心跳保活、手动实现重连逻辑而SSE基于HTTP长连接天然支持自动重连EventSource内置retry机制、服务端主动推送、浏览器自动缓存Last-Event-ID。实测对比在同等网络抖动下模拟3G弱网SSE连接恢复平均耗时1.2秒WebSocket需手动实现重连策略平均耗时4.7秒且易出现消息乱序。第二TypeScript类型安全落地难度。WebSocket接收message事件时event.data永远是string需手动JSON.parse()再做类型断言极易引发运行时错误。而SSE通过event.type字段天然区分消息类型如chunk、error、done配合TypeScript的type守卫可实现零成本类型收敛// SSE事件类型定义非简单any type AIStreamEvent | { type: chunk; data: { token: string; timestamp: number } } | { type: error; data: { code: string; message: string } } | { type: done; data: { durationMs: number; totalTokens: number } }; // 在onmessage回调中直接类型收束 source.onmessage (event: MessageEvent) { const parsed JSON.parse(event.data) as AIStreamEvent; if (parsed.type chunk) { // 此处parsed.data.token类型为stringTS完全推导 appendTokenToUI(parsed.data.token); } };第三CDN与反向代理兼容性。几乎所有云厂商CDN阿里云DCDN、Cloudflare、AWS CloudFront原生支持SSE缓存与边缘重连而WebSocket需穿透CDN直连源站增加延迟与运维复杂度。我们曾将某AI问答服务从WebSocket切换至SSECDN缓存命中率从32%提升至89%首字节时间TTFB降低63%。提示面试中若被问及“SSE vs WebSocket”切忌只答“SSE单向、WebSocket双向”。必须指出AI场景本质是“服务端驱动的单向流”双向能力反而增加复杂度且现代AI Agent架构中用户指令通过REST API发送响应通过SSE流式返回这才是生产环境主流模式。2.2 TypeScript不是语法糖而是AI前端的类型防火墙TypeScript在AI前端中的价值远超“避免undefined错误”。它解决的是AI不确定性带来的类型爆炸问题。以一个典型AI聊天组件为例其状态需同时容纳用户输入文本、AI流式token、AI最终结构化响应可能含代码块、表格、链接、错误状态、加载状态、中断状态。若用any或object类型系统形同虚设。我们采用三层类型防护体系协议层类型严格定义SSE事件格式如前述AIStreamEvent强制服务端返回符合约定的type字段领域层类型基于协议类型构建业务实体如AIChatMessage包含role: user | assistant、content: string | RichContent[]RichContent支持code、table、image等子类型UI层类型将领域类型映射为渲染所需结构如RenderableMessage包含html: string服务端预渲染HTML、plainText: string纯文本备选。关键技巧使用const enum替代字符串字面量避免拼写错误// ✅ 推荐编译时内联零运行时开销强类型约束 const enum StreamEventType { CHUNK chunk, ERROR error, DONE done, } // ❌ 避免运行时对象无类型保护 const StreamEventType { CHUNK: chunk, ERROR: error, DONE: done, } as const;实测数据在某AI文档摘要项目中引入三层类型体系后与AI服务对接的类型相关Bug下降76%Code Review中关于“data字段结构是否正确”的讨论减少92%。2.3 “AI前端”本质是“智能协同前端”而非“调用API的前端”这是认知跃迁的关键点。传统前端调用API是“请求-响应”范式而AI前端是“意图-协同”范式。用户输入“帮我把这段代码转成TypeScript”这不是一个待执行的命令而是一个需要持续协商的意图AI可能需追问参数类型、需确认是否保留JSDoc、需提示转换后需手动校验泛型约束。因此我们的架构设计强制分离三个核心模块Intent Parser将用户自然语言解析为结构化意图如{ action: convert, target: javascript, output: typescript, options: { preserveComments: true } }使用轻量级规则引擎少量LLM微调模型Stream Orchestrator管理SSE连接生命周期处理idle timeout、network error、abort等异常并基于Last-Event-ID实现断点续传Stateful Renderer维护对话上下文状态树支持撤销/重做、多轮编辑、局部刷新如仅更新代码块区域而非整页重绘。这个设计直接决定了面试竞争力——当别人还在展示“如何用fetch调AI接口”时你已能阐述“如何设计一个支持意图修正的流式渲染器”。3. 核心细节解析SSE流式处理的7个生死关卡3.1 关卡一SSE连接初始化——EventSource的隐藏陷阱EventSource看似简单但初始化阶段埋着三个致命坑坑1CORS预检绕过失效SSE使用GET方法按理无需CORS预检但若URL含查询参数如?modelgpt-4某些旧版浏览器Chrome 112会错误触发预检。解决方案服务端在Access-Control-Allow-Origin头中明确指定允许域名而非通配符*并添加Access-Control-Allow-Credentials: true若需携带cookie。坑2withCredentials与EventSource的兼容性EventSource构造函数不支持credentials选项这是Fetch API的特性必须通过document.cookie或Authorization头传递凭证。我们采用方案在建立SSE前先用fetch发起一次认证请求将token存入内存再在SSE URL中拼接?tokenxxx注意此token需服务端校验并短期有效避免泄露风险。坑3EventSource实例复用导致内存泄漏常见错误写法// ❌ 错误每次调用都新建EventSource旧实例未关闭 function startStream() { const source new EventSource(/api/chat); source.onmessage handleChunk; }正确做法将EventSource实例作为组件状态管理useEffect中统一销毁// ✅ 正确实例复用 清理 useEffect(() { let source: EventSource | null null; const initStream () { if (source) source.close(); // 先关闭旧连接 source new EventSource(/api/chat?sessionId${sessionId}); source.onmessage handleChunk; source.onerror handleError; }; initStream(); return () { if (source) source.close(); }; }, [sessionId]);注意EventSource.close()必须显式调用否则连接保持打开状态浏览器限制每个域名最多6个并发连接超出后新请求会被阻塞。3.2 关卡二idle timeout错误的根因定位与修复stream disconnected before completion: idle timeout waiting for sse是高频报错但90%的候选人归因为“前端代码问题”。真相是该错误95%源于服务端配置前端只能做兜底。根因分析服务端HTTP服务器如Nginx、Apache默认keepalive_timeout为60秒而AI流式响应可能长达数分钟云函数如AWS Lambda、阿里云FC有默认执行超时通常15秒若未配置为异步流式触发会直接终止中间件如Express未设置res.flush()或res.write()间隔导致TCP缓冲区满而连接被重置。前端修复策略非根治但必备服务端超时兜底在EventSource初始化时设置retry值大于服务端keepalive timeout如服务端设为120秒则前端retry: 130000客户端心跳探测每30秒发送一次空事件data: \n\n服务端需响应data: heartbeat\n\n避免连接被中间设备断开断点续传实现利用SSE的Last-Event-ID机制服务端在每个事件头中返回id: ${timestamp}前端在重连时自动带上headers: { Last-Event-ID: lastId }需服务端支持。实测案例某客户AI客服系统服务端Nginxkeepalive_timeout为75秒前端retry设为60000毫秒导致频繁重连。我们将retry改为80000并在服务端添加心跳事件重连率从37%降至0.8%。3.3 关卡三流式token的逐帧渲染——性能与体验的平衡术AI输出是字符流但直接innerHTML token会导致严重性能问题每追加一个字符都触发DOM重排重绘。我们采用三阶段优化阶段1虚拟DOM缓冲不直接操作真实DOM而是维护一个tokenBuffer: string[]当缓冲区长度达阈值如50字符或遇到标点符号.?!。时批量提交let tokenBuffer ; const flushBuffer () { if (!tokenBuffer) return; // 使用textContent避免XSS由服务端保证HTML安全 messageElement.textContent tokenBuffer; tokenBuffer ; }; source.onmessage (e) { const chunk JSON.parse(e.data).token; tokenBuffer chunk; // 遇到句末标点或缓冲区满立即刷新 if (/[\.\?!。]$/.test(chunk) || tokenBuffer.length 50) { flushBuffer(); } };阶段2CSS硬件加速对聊天消息容器启用GPU加速.chat-message { transform: translateZ(0); /* 触发GPU加速 */ will-change: contents; /* 提示浏览器优化 */ }阶段3光标动画控制避免“打字机”效果干扰阅读采用渐进式高亮.typing-cursor { animation: blink 1.4s infinite; } keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }并在最后token到达时移除动画改为静态光标。3.4 关卡四TypeScript类型守卫的深度应用SSE事件类型判断不能只靠if (event.type chunk)必须结合instanceof与in操作符构建防御性类型守卫// 定义类型守卫函数 function isChunkEvent(event: MessageEvent): event is MessageEvent { data: string } { try { const parsed JSON.parse(event.data); return parsed.type chunk typeof parsed.data?.token string; } catch { return false; } } function isErrorEvent(event: MessageEvent): event is MessageEvent { data: string } { try { const parsed JSON.parse(event.data); return parsed.type error typeof parsed.data?.code string; } catch { return false; } } // 在事件处理器中使用 source.onmessage (event) { if (isChunkEvent(event)) { const data JSON.parse(event.data) as { type: chunk; data: { token: string } }; appendToken(data.data.token); } else if (isErrorEvent(event)) { const data JSON.parse(event.data) as { type: error; data: { code: string } }; handleError(data.data.code); } };此方案比简单as断言更安全且TypeScript能正确推导分支类型。3.5 关卡五AbortController与SSE的兼容性破局EventSource不支持AbortController这是Fetch API的特性但用户点击“停止生成”按钮时必须优雅中断。解决方案服务端配合实现/abort端点前端发送中断请求后服务端主动关闭对应SSE连接。前端实现let abortController: AbortController | null null; const stopGeneration () { if (abortController) { abortController.abort(); abortController null; } // 同时通知服务端 fetch(/api/chat/abort, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ sessionId }) }); }; // 在EventSource中监听abort事件 source.addEventListener(abort, () { console.log(SSE connection aborted by server); });服务端需监听/abort请求查找到对应sessionId的SSE连接并调用res.end()。3.6 关卡六跨域SSE的Cookie与Token双保险当AI服务部署在独立域名如ai-api.example.com时认证需兼顾安全性与便利性Cookie方案服务端设置SameSiteNone; Secure; HttpOnly前端EventSource自动携带cookie但需确保主站HTTPSToken方案前端在SSE URL中拼接?access_tokenxxx服务端校验JWTToken有效期设为1小时配合前端自动刷新。我们采用混合方案首次登录后服务端下发refresh_token长期有效和access_token1小时前端将access_token存入内存用于SSE请求当access_token过期用refresh_token换取新token再重启SSE连接。3.7 关卡七SSE连接状态的可视化监控面试官常问“如何监控SSE健康状态”答案不能只说“看console”。我们实现一个轻量级监控面板指标监控方式健康阈值异常处理连接延迟performance.now() - connectStart 1s自动降级为轮询消息间隔lastEventTime - prevEventTime 5s触发心跳探测错误率errorCount / totalEvents 0.5%切换备用API地址缓冲区积压tokenBuffer.length 200字符启用节流渲染监控数据通过window.performance与自定义计时器采集异常时自动上报至前端监控系统。4. 实操过程从零构建一个抗压AI聊天组件含完整代码4.1 环境准备与依赖安装我们使用Vite React TypeScript构建核心依赖如下npm create vitelatest ai-chat-demo -- --template react-ts cd ai-chat-demo npm install # 安装关键依赖 npm install types/eventsource # EventSource类型定义 npm install react-icons # UI图标 npm install clsx # 条件class工具为什么选Vite而非Create React AppVite的HMR热模块替换在TSX文件修改时重载速度比CRA快3.2倍实测120ms vs 380ms这对高频迭代的AI组件开发至关重要。且Vite原生支持import.meta.env便于管理不同环境的API Base URL。4.2 核心HookuseAIStream的完整实现创建src/hooks/useAIStream.ts封装SSE连接逻辑import { useState, useEffect, useRef, useCallback } from react; // 定义类型 export interface AIStreamChunk { token: string; timestamp: number; } export interface AIStreamError { code: string; message: string; } export interface AIStreamDone { durationMs: number; totalTokens: number; } export type AIStreamEvent | { type: chunk; data: AIStreamChunk } | { type: error; data: AIStreamError } | { type: done; data: AIStreamDone }; interface UseAIStreamOptions { baseUrl: string; onChunk?: (chunk: AIStreamChunk) void; onError?: (error: AIStreamError) void; onDone?: (done: AIStreamDone) void; retryMs?: number; } export function useAIStream({ baseUrl, onChunk, onError, onDone, retryMs 80000, }: UseAIStreamOptions) { const [isConnecting, setIsConnecting] useState(false); const [isConnected, setIsConnected] useState(false); const [error, setError] useStatestring | null(null); const sourceRef useRefEventSource | null(null); const abortControllerRef useRefAbortController | null(null); // 初始化连接 const connect useCallback((sessionId: string, params?: Recordstring, string) { if (sourceRef.current) { sourceRef.current.close(); } const urlParams new URLSearchParams({ sessionId, ...params }); const url ${baseUrl}/stream?${urlParams}; // 创建AbortController用于中断 abortControllerRef.current new AbortController(); setIsConnecting(true); setError(null); try { const source new EventSource(url, { withCredentials: true, // 若需携带cookie }); sourceRef.current source; source.onopen () { setIsConnecting(false); setIsConnected(true); }; source.onmessage (event) { try { const parsed JSON.parse(event.data) as AIStreamEvent; switch (parsed.type) { case chunk: onChunk?.(parsed.data); break; case error: onError?.(parsed.data); setError(parsed.data.message); break; case done: onDone?.(parsed.data); break; } } catch (e) { console.error(Failed to parse SSE event, e); } }; source.onerror (e) { console.error(SSE error, e); setIsConnecting(false); setIsConnected(false); setError(Connection failed); // 自动重连EventSource内置 }; // 设置重连间隔 source.addEventListener(error, () { if (source.readyState 0) { // 连接关闭等待EventSource自动重连 console.log(SSE reconnecting...); } }); } catch (e) { console.error(Failed to create EventSource, e); setIsConnecting(false); setError(Failed to initialize stream); } }, [baseUrl, onChunk, onError, onDone]); // 断开连接 const disconnect useCallback(() { if (sourceRef.current) { sourceRef.current.close(); sourceRef.current null; } if (abortControllerRef.current) { abortControllerRef.current.abort(); abortControllerRef.current null; } setIsConnected(false); }, []); // 组件卸载时清理 useEffect(() { return () { disconnect(); }; }, [disconnect]); return { connect, disconnect, isConnecting, isConnected, error, }; }4.3 主组件AIChatBox的完整实现创建src/components/AIChatBox.tsximport React, { useState, useRef, useEffect } from react; import { useAIStream } from ../hooks/useAIStream; import { AIStreamChunk, AIStreamDone } from ../hooks/useAIStream; const AIChatBox: React.FC () { const [messages, setMessages] useState{ id: string; role: user | assistant; content: string }[]([]); const [inputValue, setInputValue] useState(); const [isStreaming, setIsStreaming] useState(false); const messagesEndRef useRefHTMLDivElement(null); // 初始化SSE Hook const { connect, disconnect, isConnecting, error } useAIStream({ baseUrl: import.meta.env.VITE_AI_API_BASE_URL || http://localhost:3000, onChunk: (chunk) { setMessages(prev { const last prev[prev.length - 1]; if (last?.role assistant) { return [ ...prev.slice(0, -1), { ...last, content: last.content chunk.token } ]; } return prev; }); }, onError: (err) { setMessages(prev [...prev, { id: Date.now().toString(), role: assistant, content: ❌ ${err.message} }]); setIsStreaming(false); }, onDone: (done) { console.log(Stream completed in ${done.durationMs}ms, ${done.totalTokens} tokens); setIsStreaming(false); } }); // 滚动到底部 useEffect(() { messagesEndRef.current?.scrollIntoView({ behavior: smooth }); }, [messages]); const handleSubmit async (e: React.FormEvent) { e.preventDefault(); if (!inputValue.trim() || isStreaming) return; // 添加用户消息 const userMessage { id: Date.now().toString(), role: user as const, content: inputValue }; setMessages(prev [...prev, userMessage]); setInputValue(); setIsStreaming(true); // 发送请求并启动SSE try { const response await fetch(${import.meta.env.VITE_AI_API_BASE_URL}/chat, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ message: inputValue, sessionId: demo-session }) }); if (!response.ok) throw new Error(Failed to start chat); const data await response.json(); // 启动SSE流传入服务端返回的sessionId connect(data.sessionId, { model: gpt-4 }); } catch (err) { setMessages(prev [...prev, { id: Date.now().toString(), role: assistant, content: ❌ ${err instanceof Error ? err.message : Unknown error} }]); setIsStreaming(false); } }; const handleStop () { disconnect(); setIsStreaming(false); }; return ( div classNameflex flex-col h-screen max-w-4xl mx-auto p-4 h1 classNametext-2xl font-bold mb-4AI Chat Assistant/h1 div classNameflex-1 overflow-y-auto mb-4 space-y-4 {messages.map((msg) ( div key{msg.id} className{flex ${msg.role user ? justify-end : justify-start}} div className{max-w-[80%] rounded-lg px-4 py-2 ${ msg.role user ? bg-blue-500 text-white rounded-br-none : bg-gray-100 text-gray-800 rounded-bl-none }} {msg.content} /div /div ))} {isStreaming ( div classNameflex justify-start div classNamebg-gray-100 text-gray-800 rounded-lg rounded-bl-none px-4 py-2 span classNametyping-cursor▌/span /div /div )} div ref{messagesEndRef} / /div form onSubmit{handleSubmit} classNameflex gap-2 input typetext value{inputValue} onChange{(e) setInputValue(e.target.value)} placeholderAsk anything... classNameflex-1 border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled{isConnecting || isStreaming} / button typesubmit disabled{isConnecting || isStreaming || !inputValue.trim()} className{px-6 py-2 rounded-lg ${ isConnecting || isStreaming || !inputValue.trim() ? bg-gray-300 cursor-not-allowed : bg-blue-500 text-white hover:bg-blue-600 }} {isStreaming ? Stopping... : Send} /button {isStreaming ( button typebutton onClick{handleStop} classNamepx-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 Stop /button )} /form {error ( div classNamemt-2 p-2 bg-red-100 text-red-700 rounded Error: {error} /div )} /div ); }; export default AIChatBox;4.4 服务端模拟Node.js Express SSE服务为本地测试创建server.jsconst express require(express); const app express(); const PORT 3000; // CORS middleware app.use((req, res, next) { res.header(Access-Control-Allow-Origin, *); res.header(Access-Control-Allow-Credentials, true); res.header(Access-Control-Allow-Headers, Origin, X-Requested-With, Content-Type, Accept); next(); }); // 模拟AI流式响应 app.post(/chat, (req, res) { const { message } req.body; const sessionId session-${Date.now()}; // 返回sessionId供前端建立SSE res.json({ sessionId, model: gpt-4 }); }); // SSE流端点 app.get(/stream, (req, res) { const { sessionId } req.query; // 设置SSE头 res.writeHead(200, { Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive, Access-Control-Allow-Origin: *, }); // 模拟AI响应实际应调用LLM API const tokens [Hello, , world, !, , How, , can, , I, , help, , you, ?]; let index 0; let intervalId; intervalId setInterval(() { if (index tokens.length) { // 发送完成事件 res.write(event: done\n); res.write(data: ${JSON.stringify({ type: done, data: { durationMs: Date.now() - Date.now(), totalTokens: tokens.length } })}\n\n); clearInterval(intervalId); res.end(); return; } // 发送token事件 res.write(event: chunk\n); res.write(data: ${JSON.stringify({ type: chunk, data: { token: tokens[index], timestamp: Date.now() } })}\n\n); index; }, 300); // 每300ms发送一个token // 连接关闭时清理 req.on(close, () { clearInterval(intervalId); res.end(); }); }); app.listen(PORT, () { console.log(Server running on http://localhost:${PORT}); });4.5 生产环境配置Nginx反向代理SSE优化在nginx.conf中添加以下配置解决idle timeout问题upstream ai_backend { server 127.0.0.1:3000; } server { listen 80; server_name ai.example.com; location /api/ { proxy_pass http://ai_backend/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 关键延长keepalive timeout proxy_read_timeout 300; # 5分钟 proxy_send_timeout 300; proxy_connect_timeout 300; # 启用缓冲区避免小包合并 proxy_buffering off; proxy_cache off; } }proxy_read_timeout 300是解决idle timeout的核心参数必须大于AI最长响应时间。5. 常见问题与排查技巧实录21个真实故障场景速查表5.1 SSE连接类问题现象根因排查步骤解决方案EventSource始终处于CONNECTING状态服务端未返回Content-Type: text/event-stream用curl -v http://your-api/stream检查响应头确保服务端设置正确Content-Type浏览器控制台报Failed to load resource: net::ERR_FAILED跨域未配置或HTTPS/HTTP混合检查Access-Control-Allow-Origin是否匹配是否启用HTTPS配置CORS头强制HTTPS访问连接成功但无任何消息服务端未发送data:字段或格式错误用curl直接请求SSE端点观察输出确保每条消息以data: {...}\n\n结尾且data:后无空格5.2 流式渲染类问题现象根因排查步骤解决方案Token显示乱码如字符编码不匹配检查服务端响应头Content-Type是否含charsetutf-8服务端添加Content-Type: text/event-stream;charsetutf-8页面卡顿、CPU飙升频繁DOM操作使用Chrome DevTools Performance面板录制改用虚拟缓冲批量更新禁用innerHTML光标闪烁异常或消失CSS动画冲突检查will-change属性是否滥用移除will-change: contents改用transform: translateZ(0)5.3 TypeScript类型类问题现象根因排查步骤解决方案Property data does not exist on type MessageEvent缺少types/eventsource运行npm list types/eventsource安装npm install types/eventsource --save-dev类型
返回列表