实现chatGpt的效果,包含以下几个技术点:
1. 流式获取
要实现像chatgpt一个字一个字回复的效果,涉及到流式获取数据SSE(Server - Sent Events)。
sse利用向客户端声明,接下来要发送的是流信息的机制,向浏览器连续不断地推送信息。
这完全不同于与平常我们所用的一次性加载方式。在传统方式中,前端发起请求后,需要等待后端准备好完整数据才能一次性返回;而流式方式则是后端逐步返回数据片段(chunk),前端逐块接收和处理,实现"边接收边处理"的效果。
如何实现:
- 后端实现
后端设置响应头:(记得添加charset=utf-8,声明编码格式,防止中文乱码
vbnet
text/event-stream;charset=utf-8

响应体格式:
每一次发送的信息,由若干个message
组成,每个message
之间用\n\n
分隔。每个message
内部由若干行组成,每一行都是如下格式。
markdown
[field]: value\n
上面的field
可以取四个值。
- data
- event
- id
- retry
返回示例:
vbnet
event: userconnect
data: {"username": "bobby", "time": "02:33:48"}
data: {"username": "bobby", "time": "02:34:11", "text": "Hi everyone."}
data: {"username": "bobby", "time": "02:34:23"}
data: {"username": "sean", "time": "02:34:36", "text": "Bye, bobby."}
# Server-Sent Events 教程-阮一峰- 4.1 数据格式
用nodejs,express实现一个简易的sse接口:
js
const express = require('express');
const cors = require('cors');
const app = express();
const port = 3000;
// 配置CORS选项
const corsOptions = {
origin: true, // 或指定域名如 'http://example.com'
methods: ['GET', 'OPTIONS'], // SSE通常使用GET
allowedHeaders: ['Content-Type'],
credentials: true, // 如果需要凭证
};
// 应用CORS中间件
app.use(cors(corsOptions));
app.get('/sse', (req, res) => {
const text = '随便写一句话,看看效果';
const speed = 100;
res.writeHead(200, {
'Content-Type': 'text/event-stream;charset=utf-8',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
// 添加cors
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Credentials': true,
});
let index = 0;
const timer = setInterval(() => {
if (index < text.length) {
res.write(`data: ${text[index]}\n\n`);
index++;
} else {
clearInterval(timer);
res.write('event: end\ndata: stream ended\n\n');
res.end();
}
}, speed);
req.on('close', () => {
clearInterval(timer);
res.end();
});
});
app.listen(port, () => {
console.log(`SSE Typewriter server running at http://localhost:${port}`);
});
如果是nginx部署,需注意添加接口配置
关闭代理缓冲 proxy_buffering off
;详见处理 EventStream 不能流式返回的问题:Nginx 配置优化
- 前端实现:
利用fetch api处理,示例如下:
js
<!DOCTYPE html>
<html>
<head>
<title>获取流式数据 Demo</title>
<style>
#output {
font-family: monospace;
font-size: 24px;
min-height: 100px;
border: 1px solid #ccc;
padding: 10px;
white-space: pre-wrap;
}
</style>
</head>
<body>
<button id="startBtn">获取流式数据</button>
<div id="output"></div>
<script>
document
.getElementById('startBtn')
.addEventListener('click', async () => {
const output = document.getElementById('output');
output.innerHTML = ''; // 清空输出
try {
const response = await fetch('http://localhost:3000/sse', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 获取可读流
const reader = response.body.getReader();
const decoder = new TextDecoder();
// 递归读取流
const readChunk = async () => {
const { done, value } = await reader.read();
if (done) {
return;
}
// 解码并显示字符
const chunk = decoder.decode(value);
const text = chunk.split('data: ')[1].replace('\n\n', '');
console.log(text);
output.innerHTML += text || '';
// 继续读取下一个字符
readChunk();
};
readChunk();
} catch (error) {
console.error('Error:', error);
output.textContent = 'Error: ' + error.message;
cursor.remove();
}
});
</script>
</body>
</html>
这里前端处理方式还有eventSoure对象,但有局限,只支持get请求。以及相关库实现@microsoft/fetch-event-source。流式处理前端方案对比详见:juejin.cn/post/747810... juejin.cn/post/747849...
2. 体验优化
- 暂停控制:通过AbortController中断流
示例:
js
const controller = new AbortController(); //创建AbortController对象
async function streamData() {
try {
const response = await fetch('/stream-endpoint', {
signal: controller.signal // 添加signal
});
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log('收到数据块:', value);
}
} catch (err) {
if (err.name === 'AbortError') {
console.log('流已中断');
} else {
console.error('流错误:', err);
}
}
}
// 中断流
function cancelStream() {
controller.abort(); // 在合适时机触发abort,取消请求
}
- 智能滚动:仅当用户未手动滚动时自动滚动到底部
js
// 智能滚动:仅当用户未手动滚动时生效
if (isNearBottom()) container.scrollTop = container.scrollHeight;
// 判断是否接近底部
isNearBottom() {
const { scrollTop, scrollHeight, clientHeight } = this.container;
return scrollHeight - (scrollTop + clientHeight) < this.threshold;
}
this.threshold = 10; // 距离底部的阈值(px),10,为举例