前端长链接架构设计:WebSocket数据流实现AI对话页
背景
最近新增了一个产品项目“今天炒什么”,这是一款面向短线用户的热点选股工具。展示每日精选的热点话题,及相关话题近一个月的热度值。有一个“热点解读”功能,需要调用大模型接口实现一个AI对话页。其中大模型接口为流式响应,所以决定采用 WebSocket 技术实现前后端实时通信,确保为用户提供流畅自然的对话体验。
1 架构设计
1.1 前端对话页功能设计
- 建立 WebSocket 连接,确认连接状态
- 发送用户消息
- 接收并实时渲染 AI 回复
- 实现对话打字机效果
1.2 通信流程
- 连接建立:
- 通过
new WebSocket('ws://xxxxxx')创建实例,绑定事件处理函数并建立连接
- 通过
- 消息发送:
- 用户输入消息后,前端通过
ws.send()发送 JSON 格式消息
- 用户输入消息后,前端通过
- 流式响应:
- 大模型接口每生成一个字符,立即通过 WebSocket 推送给前端
- 前端收到字符后,实时更新界面,实现打字机效果
- 连接管理:
- 通过
onmessage事件接收后端推送的数据,实时更新界面 - 前端监听
onopen、onmessage、onclose、onerror事件,管理连接状态
- 通过
2 核心代码解析
2.1 前端 WebSocket 连接
useEffect(() => {
const ws = new WebSocket('ws://127.0.0.1:3000');
wsRef.current = ws;
ws.onopen = () => {...};
ws.onclose = () => {...};
ws.onerror = (error) => {...};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// 处理不同类型的消息:start、token、end、error
};
return () => {
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
};
}, []);
2.2 建立 WebSocket 连接
在前端,我们通过 new WebSocket() 创建一个 WebSocket 实例,并绑定事件处理函数:
const ws = new WebSocket('ws://xxxxxx');
ws.onopen = () => {
console.log('WebSocket连接已建立');
setIsConnected(true);
};
ws.onclose = () => {
console.log('WebSocket连接已关闭');
setIsConnected(false);
};
ws.onerror = (error) => {
console.error('WebSocket错误:', error);
setIsConnected(false);
};
2. 3 发送消息
用户输入消息后,前端通过 ws.send() 发送 JSON 格式数据:
// 输入框提交文本事件
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!inputValue.trim() || !isConnected) return;
// 用户提问信息
setMessages(prev => [...prev, { role: 'user', content: inputValue }]);
// 像大模型接口发送提问消息
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'message',
content: inputValue
}));
}
// 清空输入框
setInputValue('');
};
2.4 接收流式响应
前端通过 onmessage 事件接收后端推送的数据,并根据消息类型实时更新界面:
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'start':
// 开始新的AI响应,新增一条机器人消息
setMessages(prev => [...prev, { role: 'assistant', content: '' }]);
setIsTyping(true);
break;
case 'token':
// 接收新的token并更新消息
setMessages((prev: Message[]) => {
const newMessages = [...prev];
const lastIndex = newMessages.length - 1;
if (lastIndex >= 0 && newMessages[lastIndex].role === 'assistant') {
return [
...newMessages.slice(0, lastIndex),
{
...newMessages[lastIndex],
content: newMessages[lastIndex].content + data.token
}
];
}
return newMessages;
});
break;
case 'end':
// 响应结束
setIsTyping(false);
break;
case 'error':
// 处理错误
console.error('WebSocket错误:', data.error);
setIsTyping(false);
break;
}
};
3 其他弃用方案
在方案的设计过程中,后端提出了一种方案:自己调用大模型接口,等文本全部返回后再返回给前端。这样前端就可以直接渲染整段文本,不需要接收实时渲染的数据。
核心逻辑就是使用typed.js 插件渲染文本,实现打字机效果,关键代码如下:
import Typed from 'typed.js';
const TypedReact = (text) => {
const typedElement = useRef(null);
const typed = useRef(null);
useEffect(() => {
if (typedElement.current) {
// 初始化 Typed 实例
typed.current = new Typed(typedElement.current, {
strings: [text],
typeSpeed: 100,
startDelay: 500,
showCursor: true,
cursorChar: '|',
autoInsertCss: true,
onComplete: (self) => {
// 打字完成后的回调
}
});
}
// 组件卸载时清理 Typed 实例
return () => {
if (typed.current) {
typed.current.destroy();
}
};
}, [text]); // 当这些属性变化时重新初始化
return (
<div className='typed-container'>
<span ref={typedElement}></span>
</div>
);
};
由于大模型接口答案较长,在全部响应完成之前loading时间过长,所以最终决定弃用此方案。

浙公网安备 33010602011771号