需求
需要使用react实现模拟chatGPT的页面,后端接口使用流式传输 stream: true,并且用 POST 方法进行传参
前端方法
用原始的方案
大概思路:使用 fetch 接受数据,然后读取数据流,解析数据,获取到需要的数据结构,然后再封装展示的方法,html 用原生js获取id实现页面的展示,展示的过程中自定义定时器,实现打字机的效果
以下代码:
if (input.trim()) {
const streamUrl = '/opentrek-chat/completions';
fetch(streamUrl, {
method: 'POST',
headers: {
'Content-Type': '',//根据需求写
'Authorization': '', //根据需求写
},
body: JSON.stringify({
"messages": [
{
"role": "user",
"content": "全新CT6照明功能介绍"
},
],
"stream": true,
}),
})
.then(response => {
const decoder = new TextDecoder('utf-8');
const reader = response.body.getReader();
let buffer = ''; // 用于累计流数据
let allMessagesArr = []; // 用于存储所有的消息内容
// 使用 async/await 来简化递归流读取
async function readStream() {
try {
const { done, value } = await reader.read();
if (done) {
console.log('流读取完成');
// 解析并处理所有流数据
handleMessages();
return;
}
// 解码流数据并更新 buffer
const chunk = decoder.decode(value, { stream: true });
buffer += chunk;
readStream(); // 继续读取流数据
} catch (error) {
console.error("读取流数据出错:", error);
}
}
// 处理流数据并提取消息
function handleMessages() {
// 以行为单位拆分消息
const messages = buffer.split('\n')
.filter(item => item.trim() !== '' && item.startsWith('data: '))
.map(item => item.slice(5).replace(/\n$/, '').trim()) // 去掉 'data: ' 和末尾的换行
.filter(item => item !== '[DONE]') // 忽略 '[DONE]' 标记
.map(item => {
try {
return JSON.parse(item)?.message?.content; // 解析 JSON 数据并提取 content
} catch (e) {
console.error("消息解析失败:", e);
return null;
}
})
.filter(Boolean); // 去除无效数据
// 将所有有效消息添加到 allMessagesArr 中
allMessagesArr = allMessagesArr.concat(messages);
displayMessages(allMessagesArr); // 调用函数展示消息
}
// 展示逐条输出消息
async function displayMessages(messages) {
let index = 0;
const typingElement = document.getElementById('typing-element'); // 假设这是你要展示消息的 DOM 元素
async function type() {
if (index < messages.length) {
typingElement.textContent += messages[index]; // 展示当前消息
index++;
await new Promise(resolve => setTimeout(resolve, 50)); // 延时 50ms
type(); // 递归调用实现逐条输出
}
}
// 调用逐条输出消息的函数
await type();
// 更新 React 状态,展示所有有效消息
setMessages(prevMessages => [...prevMessages, ...messages]);
}
// 开始读取流数据
readStream();
})
.catch(error => {
console.error('请求出错:', error);
});
}
用SSE实现方案
如果后端是get请求的话,可以直接用SSE的方案,一下是一个使用vue实现的demo
react实现的方法大差不差
GET请求
<template >
<div>
<h1>Streamed Data</h1>
<div v-for="(message, index) in messages" :key="index">
{{ message }}
</div>
</div >
</template >
<script>
export default {
data() {
return {
messages: [], // 保存从服务端接收到的流式数据
};
},
methods: {
connectToStream() {
// 创建 EventSource 对象,连接后端流式接口
const eventSource = new EventSource(`http://192.168.254.200:8081/ask/stream`);
// 监听 'message' 事件(默认事件)
eventSource.onmessage = (event) => {
try {
const data = event.data; // 解析 JSON 数据
this.messages.push(data); // 将数据添加到消息列表
} catch (error) {
console.error("Error parsing JSON:", error);
}
};
// 错误处理
eventSource.onerror = (error) => {
console.error("EventSource failed:", error);
eventSource.close(); // 关闭连接
};
},
},
mounted() {
this.connectToStream(); // 在组件挂载时启动 SSE 连接
},
};
</script>
<style>
/* 可选样式 */
h1 {
color: #333;
}
div {
font-family: Arial, sans-serif;
margin: 10px 0;
}
</style>
POST请求
EventSource(SSE):是一种持久的连接,用于从服务器向客户端推送实时更新,通常是 GET 请求,它会持续保持连接,不会像普通的请求那样一次性响应数据。
所以我们需要通过转化,来实现POST获取SSE的数据流
需要下载插件:@microsoft/fetch-event-source
npm install @microsoft/fetch-event-source
或
yarn add @microsoft/fetch-event-source
//在代码当中进行引入
import { fetchEventSource } from '@microsoft/fetch-event-source';
//这个方法写到触发的事件当中
async function startSseWithPost() {
try {
// 启动服务器事件源请求
await fetchEventSource('/opentrek-chat/v2/completions', {
method: 'POST',
headers: {
'Content-Type': '',//看项目需求
'Authorization': '', //看项目需求
},
body: JSON.stringify({
"messages": [
{
"role": "user",
"content": "全新CT6照明功能介绍"
},
],
"stream": true,
}),
onopen(response) {
// 连接建立成功时的回调
console.log('连接已建立:', response);
},
onmessage(event) {
// 处理收到的消息
if (event.data) {
try {
const messageData = JSON.parse(event.data);
if (messageData?.message?.content) {
// 显示接收到的消息内容,封装的方法
displayMessage(messageData.message.content);
} else {
console.error('接收到的数据没有有效的消息内容');
}
} catch (error) {
// 解析消息数据时出现错误
console.error('解析消息数据时出错:', error);
}
}
},
onerror(err) {
// 发生错误时的回调
console.error('SSE 发生错误:', err);
},
});
} catch (error) {
// 启动 SSE 连接时的错误处理
console.error('启动 SSE 失败:', error);
}
}
function displayMessage(messageContent) {
// 获取消息显示的容器
const messageContainer = document.getElementById('text');
if (!messageContainer || !messageContent) {
console.error('无效的消息内容或容器未找到');
return;
}
// 创建新的元素来显示消息
const newMessageElement = document.createElement('p');
messageContainer.appendChild(newMessageElement);
// 使用 requestAnimationFrame 进行逐字显示效果
let index = 0;
const typingSpeed = 100; // 每个字符的显示间隔时间(毫秒)
// 清空之前的消息内容
newMessageElement.textContent = '';
// 定义逐字显示的函数
function typeNextCharacter() {
if (index < messageContent.length) {
newMessageElement.textContent += messageContent[index]; // 添加下一个字符
index++;
// 使用 requestAnimationFrame 来平滑显示字符
setTimeout(() => requestAnimationFrame(typeNextCharacter), typingSpeed);
}
}
// 开始逐字显示消息
requestAnimationFrame(typeNextCharacter);
}
// 启动 SSE 流
startSseWithPost();