共计 3057 个字符,预计需要花费 8 分钟才能阅读完成。
背景痛点分析
构建类 ChatGPT 前端界面面临三个核心挑战:

- 实时流式渲染:传统请求 - 响应模式无法满足逐字输出的用户体验需求,需处理分段到达的数据流
- 复杂状态管理:对话历史、生成状态、错误恢复等多维度状态的同步与持久化
- 内容呈现兼容性:需同时支持纯文本、Markdown 语法、代码块等多种内容格式的高亮与排版
技术选型对比
| 框架 | 实时渲染性能 | TypeScript 支持 | 状态管理复杂度 | 生态工具链 |
|---|---|---|---|---|
| React | ★★★★☆ | 原生支持 | 需要 Redux 等库 | 最丰富 |
| Vue | ★★★☆☆ | 需额外配置 | Pinia 内置方案 | 较完善 |
| Svelte | ★★★★★ | 支持一般 | 内置响应式 | 新兴但不足 |
选择 React+TypeScript 组合的核心优势:
- 完善的虚拟 DOM 差异更新机制适合高频内容变更
- 类型系统可规范对话数据结构(如消息角色、元信息等)
- 成熟的性能优化方案(memo、useCallback 等)
核心实现路径
基础架构搭建
// 定义核心数据类型
interface Message {
id: string;
role: 'user' | 'assistant';
content: string;
createdAt: number;
}
// 使用 Context 管理对话状态
const ChatContext = createContext<{messages: Message[];
appendMessage: (msg: Message) => void;
streaming: boolean;
}>(/*...*/);
流式响应处理
async function fetchStream(query: string) {
const response = await fetch('/api/chat', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({query})
});
if (!response.body) throw new Error('No readable stream');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {const { done, value} = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true});
// 处理可能的多个消息块
const chunks = buffer.split('\ndata:');
buffer = chunks.pop() || '';
for (const chunk of chunks) {if (chunk.trim()) {const data = JSON.parse(chunk);
updateMessage(data); // 增量更新 DOM
}
}
}
}
虚拟滚动优化
// 使用 react-window 库实现
import {FixedSizeList as List} from 'react-window';
const VirtualizedList = ({messages}: {messages: Message[] }) => (
<List
height={600}
itemCount={messages.length}
itemSize={120}
width="100%"
>
{({index, style}) => (<div style={style}>
<MessageItem message={messages[index]} />
</div>
)}
</List>
);
Markdown 渲染方案
推荐使用 react-markdown+rehype-highlight 组合:
import ReactMarkdown from 'react-markdown';
import rehypeHighlight from 'rehype-highlight';
<ReactMarkdown
rehypePlugins={[rehypeHighlight]}
components={{code({node, inline, className, children, ...props}) {const match = /language-(\w+)/.exec(className || '');
return !inline ? (
<SyntaxHighlighter
language={match?.[1] || 'javascript'}
PreTag="div"
{...props}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (<code className={className} {...props}>
{children}
</code>
);
}
}}
>
{content}
</ReactMarkdown>
生产环境优化
WebSocket 重连策略
class ChatSocket {
private socket: WebSocket | null = null;
private retries = 0;
private maxRetries = 3;
connect() {this.socket = new WebSocket(ENDPOINT);
this.socket.onclose = () => {if (this.retries < this.maxRetries) {const delay = Math.min(5000, 1000 * Math.pow(2, this.retries));
setTimeout(() => {
this.retries++;
this.connect();}, delay);
}
};
}
}
内存管理技巧
- 对话历史分页加载
- 超过 100 条消息时自动归档旧会话
- 使用 WeakMap 缓存已渲染的 Markdown AST
移动端适配要点
/* 防止 iOS 输入法遮挡 */
.chat-input {
position: fixed;
bottom: 0;
padding-bottom: env(safe-area-inset-bottom);
}
常见问题解决方案
-
状态更新竞争条件:
// 使用函数式更新确保顺序 setMessages(prev => [...prev, newMsg]); -
Markdown XSS 防护:
import rehypeSanitize from 'rehype-sanitize'; <ReactMarkdown rehypePlugins={[rehypeSanitize]} /> -
快速输入导致重复请求:
const [pending, setPending] = useState(false); useEffect(() => { let timer: NodeJS.Timeout; if (pending) {timer = setTimeout(() => setPending(false), 1000); } return () => clearTimeout(timer); }, [pending]);
进阶优化方向
- 使用 Server-Sent Events(SSE)替代 WebSocket 简化协议
- 通过 WebAssembly 加速 Markdown 解析(如使用
markdown-rs-wasm) - 对话内容压缩存储(使用 LZString 压缩 localStorage 数据)
- 实现对话快照功能(利用 Canvas 生成会话截图)
完整示例项目可参考 GitHub 仓库:https://github.com/example/chatgpt-ui
正文完
发表至: 前端开发
近一天内
