好记性不如烂笔头。
1、前言
大家好,我是黑翼。写得不好,不喜勿喷。
2、背景
本篇博客的背景,来自于工作中的大模型业务需求。作为该需求的前端开发,在完成工作过程中,学习了很多,也遇见了一些坑,因此记录并分享。
3、SSE
在今年ChatGPT爆火之后,SSE这门通信技术也随之得到了更多的关注。之所以其在大模型中应用得比较多,就是因为'打字机'效果,让用户觉得交互体验比较好。当然,使用SSE的原因也不仅仅如此。
我们选择SSE方案有以下原因:
- 1、SSE比较轻,相较于Websocket;
- 2、基于Http协议;
- 3、是流式、单工(服务端 => 客户端)的;
- 4、可以用fetch实现;
- 5、向chatgpt等平台看齐;
4、兼容性
SSE具有广泛的的浏览器兼容性,除IE之外的浏览器几乎均支持。
5、fetch实现
看了部分文章以及通过chrome调试工具观察某chatgpt助手,发现使用fetch即可实现流式交互。其实,对于前端而言,目前用的更广泛还是axios库,对axios也可能了解更多一些,所以完成该需求,也让我对fetch有了更深的理解。。
本节部分会先提一些知识点,最后会附上完成(脱敏)代码。
5.1 配置
配置包括前端配置、服务端配置,本质是浏览器发起 http 请求,服务器在收到请求后,流式返回状态与数据后进行处理,实现'打字机'效果。
前端配置主要包括以下:
js
let options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'text/event-stream',
'Sec-Fetch-Site': 'same-origin',
},
credentials: 'include',
body: JSON.stringify({
data
}),
};
Accept:SSE API规定推送事件流的 MIME 类型为 text/event-stream。
credentials: 携带cookie凭证;
而服务端配置主要包括(但不限于)以下:
js
proxy_http_version 1.1;
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
gzip off;
chunked_transfer_encoding off;
keepalive timeout 300
如果上述服务器配置还未能支持'流式交互',在确定不是代码问题之后,务必关注这一点。
个人经历:如果后端使用了一些公司内部组件的话,需要排查该组件是否会导致'流式交互'失败。
5.2 消息格式
EventStream(事件流)为 UTF-8 格式编码的文本或使用 Base64 编码和 gzip 压缩的二进制消息。
每次推送,可由多个消息组成,每个消息之间以空行分隔(即最后一个字段以\n\n结尾)。
这个是比较重要的特点,方便解析每条推送消息,实现'打字机'效果。
5.3 Complete
当后端将所有数据推送完后,需要向前端推送一条Complete消息,浏览器监听该消息后将done置为true,表示整个响应完整结束了。
如果不发送complete消息,浏览器会报错 - net::ERR_INCOMPLETE_CHUNKED_ENCODING 200 (OK)。
5.4 timeout
由于面向的是业务需求,而且fetch的超时与axios的超时也有所区别,所以这里单独提一下。
与axios不同,fetch的超时使用的是AbortController,具体见后续代码。
5.5 完整代码
js
const controller = new AbortController();
const timeout = setTimeout(() => {
controller.abort();
console.error('查询超时,取消请求!');
}, 1000 * 60 * 3);
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'text/event-stream'
},
credentials: 'include',
body: JSON.stringify({
data
}),
signal: controller.signal,
timeout,
};
this.fetchEventSource(this.SSEUrl, options);
fetchEventSource(url, options) {
fetch(url, options)
.then((response) => {
const reader = response.body.getReader();
let buffer = '';
const decoder = new TextDecoder();
const read = () => {
return reader.read().then(({ done, value }) => {
options.timeout && clearTimeout(options.timeout);
if (done) {
return;
}
buffer += decoder.decode(value);
const parts = buffer.split('\n\n');
buffer = parts.pop() || ''; // 保留不完整的部分
parts.forEach(part => {
this.parseSSEEvent(part);
});
// 继续读取下一个数据块
read();
});
};
// 开始读取数据流
read();
})
.catch(() => {
options.timeout && clearTimeout(options.timeout);
});
}
parseSSEEvent(data: any) {
try {
data.split('\n').forEach(line => {
// 处理数据
....
});
} catch (error) {
console..error('数据解析失败!');
}
}
6、总结
在接触新技术、新需求时,'摸着石头过河'可能会是比较常见的情形。因此,在前行的路上,不妨多回头看,做一些阶段性总结,记录一些经历。总之,好记性不如烂笔头。