fetch EventSource 请求,vue2 项目 webpack 没有流式输出

背景

最近做类似AI会话gpt流式输出的效果,借助 vue 的数据响应,可轻松实现流式输出,在 vue3+vite 的架构中,可以实现流式输出,今天同样功能特定客户需要vue2 + webpack来实现,要我写个demo,代码逻辑迁移过去换个写法而已,简单。。 可是写完后发现无法流式输出,总是一下子全部返回数据,即使设置了流式返回。

请求代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
fetch(`/llmops/chat/apps/348/conversations/${this.currentChatParam.conversationId}/qas/${qaId}/stream`, {
method: 'POST',
headers: {
'Accept': 'text/event-stream',
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({ chatLogId })
})
.then(response => {
if (response.ok) {
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = ''; // 用于存储解码后的完整数据
this.responseData = [];
this.idx = 0;

const processStream = ({ done, value }) => {
if (done) {
console.log('getResAnswer-4', Date.now());
// 如果流结束,处理剩余数据
if (buffer) {
const lines = buffer.split('\n').map(it => it.trim()).filter(it => it);
// 不再使用pop来移除最后一行,改为检查所有行的数据
lines.forEach(line => {
const str = line.trim().split('data:')[1];
if (str) {
const parsedData = JSON.parse(str);
this.responseData.push({
answer: parsedData.answer,
answerType: 'text',
isEnd: parsedData.isEnd === 1, // 判断是否为最后一条数据
});
}
});
}
return;
}

// 解码当前 chunk 并追加到缓冲区
buffer += decoder.decode(value, { stream: true });

// 处理缓冲区数据,按行拆分
let lines = buffer.split('\n').map(it => it.trim()).filter(it => it);
buffer = lines.pop(); // 取出未完成的最后一行,留待下一次

lines.forEach((line, idx) => {
const str = line.trim().split('data:')[1];
if (str) {
const parsedData = JSON.parse(str);
const isEnd = parsedData.isEnd === 1; // 判断是否为最后一条数据
this.responseData.push({
answer: parsedData.answer,
answerType: 'text',
isEnd, // 根据 parsedData 的 isEnd 字段来确定是否结束
});
}
});

// 递归处理流中的下一块数据
return reader.read().then(processStream);
};

// 启动流的读取
reader.read().then(processStream);
} else {
console.error('Failed to connect to the stream:', response.status);
}
})
.catch(error => console.error('Fetch error:', error));

解决方法

webpack中 默认开启了 compress,在 vue.config.js 设置为false,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
devServer: {
host: "0.0.0.0",
port: 5001,
open: true,
compress: false,
proxy: {
"/aaaaaa/": {
// ngnix 代理了/ragapi
target: "http://aaaaaaaaa/", // 线上地址
changeOrigin: true,
secure: false,
},
},
},

此时可流式输出,vite中 compress 默认是 false 所以不用设置就可流式输出

nginx配置

1
2
3
4
5
6
7
8
location / {
add_header Cache-Control no-cache;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
}

参数解释

在 vue.config.js 文件中,devServer 是一个用于配置 Vue CLI 项目开发服务器的对象。当你设置 compress: false 时,你实际上是在告诉开发服务器不要对响应的内容进行压缩。

compress 选项的作用通常是控制开发服务器是否应该使用 gzip 或其他压缩算法来压缩发送给浏览器的响应内容。当设置为 true 时,服务器会尝试压缩响应,这通常可以减少传输的数据量,因为压缩后的内容通常比原始内容更小。但是,这也需要额外的 CPU 时间来进行压缩和解压缩操作。
在开发环境中,compress: false 可能有以下好处:

  1. 更快的响应速度:由于不需要进行压缩和解压缩操作,服务器可以更快地发送响应给浏览器。
  2. 避免压缩相关的错误:有时,压缩算法可能会与某些内容类型或代码不兼容,导致在浏览器中出现显示问题或错误。在开发环境中禁用压缩可以避免这些问题。
  3. 简化调试:未压缩的响应内容更易于阅读和调试,特别是当你正在查看网络请求和响应时。

然而,在生产环境中,你通常会希望启用响应压缩(例如,通过设置 HTTP 头部 Content-Encoding: gzip),以减少网络传输的开销和提高用户体验。这通常是在 Web 服务器(如 Nginx、Apache)或 CDN 层面上配置的,而不是在应用程序的开发服务器中。

总之,compress 选项在 vue.config.js 的 devServer 配置中用于控制开发服务器是否应该对响应内容进行压缩。将其设置为 false 可以加快开发过程中的响应速度,并避免与压缩相关的潜在问题。