前言
当前有一个需求,当前需要依靠minio桶进行创建任务,而可能会出现一个问题就是504错误(Gateway Timeout),当前如果桶的任务数太大的话,后端服务器可能需要更长时间来完成请求,前台就迟迟得不到响应。
问题分析
这时候我们的解决思路就是希望后端处理的时候,返回一些进度信息,而前台接受到响应就不会出现504(Gatway Timeout),所以我们希望实现的效果就是使用sse的方式进行解决这个问题
实现案例
实现的效果
服务端实现
public void initBucket(List<MinioObject> minioObjects, HttpServletResponse response) {
response.setContentType("text/event-stream");
response.setCharacterEncoding("UTF-8");
try (PrintWriter writer = response.getWriter()) {
int totalCount = minioObjects.size(); // 处理项的总数
int currentCount = 0; // 当前处理的项数
for (MinioObject minioObject : minioObjects) {
currentCount++;
double progressPercentage = ((double) currentCount / totalCount) * 100;
writer.print(String.format("%.2f", progressPercentage) +"\n\n");
writer.flush();
}
writer.print("complete\n\n");
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
响应设置
将响应内容类型设置为 text/event-stream,以启用服务端推送事件(Server-Sent Events, SSE)。
response.setContentType("text/event-stream");
response.setCharacterEncoding("UTF-8");
获取输出流:
使用PrintWriter数输出流,向前台发送数据
try (PrintWriter writer = response.getWriter()) {
初始化总数和计数器
int totalCount = minioObjects.size(); //3
int currentCount = 0;
更新进度并返回给前台
for (MinioObject minioObject : minioObjects) {
currentCount++;
double progressPercentage = ((double) currentCount / totalCount) * 100;
writer.print(String.format("%.2f", progressPercentage) +"\n\n");
writer.flush();
}
// 第一次返回33.33
// 第二次返回66.67
// 第三次返回100
发送完成标记
告诉前端当前发送完毕
writer.print("complete\n\n");
writer.flush();
前端实现
使用SSE一般在EventSource对象上,但是使用EventSource上有一个问题,就是无法设置请求头,也就是是当前后台如果有权限认证没办法使用,传递token得方式一般只能带在url上,显然这种方式不符合要求,而且使用这个的话也只能发送GET请求
const eventSource = new EventSource('api/bucket/init?token=xxxx');
使用Fetch APi请求数据
fetch API 是一个用于在 JavaScript 中发起网络请求的接口。它是基于 Promise
基本语法
fetch(url, options)
.then(response => {
// 处理响应数据
})
.catch(error => {
// 处理错误
});
通过 Fetch API 请求流式数据,同时添加了 x-auth-token 到请求头中。接受后台传递过来的流式数据
createEventSourceWithToken(url: string, method = 'GET'): Observable<any> {
const xAuthToken = XAuthTokenInterceptor.getToken();
const headers = new Headers();
headers.append('x-auth-token', xAuthToken);
return new Observable<any>((observer) => {
fetch(url, {method: method, headers: headers,}).then(response => {
if (!response.ok) {
throw new Error('Authorization failed');
}
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
const readStream = (): Promise<void> => {
return reader.read().then(({done, value}) => {
if (done) {
observer.complete();
return Promise.resolve();
}
// 将 Uint8Array 转换为字符串
const text = decoder.decode(value, {stream: true});
const events = text.split('\n'); // 以换行符
events.forEach(event => {
this.zone.run(() => {
observer.next(event);
});
});
return readStream();
});
};
return readStream();
}).catch(error => {
this.zone.run(() => {
observer.error(error);
});
});
});
}
设置请求头
const xAuthToken = XAuthTokenInterceptor.getToken();
const headers = new Headers();
headers.append('x-auth-token', xAuthToken);
fetch(url, {method: method, headers: headers})
处理响应
当有返回响应成功后,调用 then(Fetch 返回是 Promise)方法拿到后台传递的数据,之后读取后台传递的二进制格式(Uint8Array)的数据,再使用Web API 提供的 TextDecoder 用于将 Uint8Array(即二进制数据块)解码成可读文本字符串。解码器会根据给定编码方式(这里是 utf-8 )解码数据。
.then(response => {
if (!response.ok) {
throw new Error('Authorization failed');
}
// 读取的数据以二进制格式(Uint8Array)。ReadableStreamDefaultReader
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
读取流式数据:
- read():从流中读取下一块数据,返回一个 Promise。
- 如果成功读取到数据块,它会返回一个包含 value 和 done 属性的对象。
- value:当前的数据块。
- done:指示是否已到达流的结尾,true 表示流已结束,false 表示还有数据。
const readStream = (): Promise<void> => {
return reader.read().then(({done, value}) => {
if (done) {
observer.complete();
return Promise.resolve();
}
// 将 Uint8Array 转换为字符串
const text = decoder.decode(value, {stream: true});
const events = text.split('\n'); // 以换行符分割事件
events.forEach(event => {
this.zone.run(() => {
observer.next(event);
});
});
return readStream();
});
};
组件进行调用
BucketComponent组件
isDialogOpen = false;
progress = 0; // 进度值
constructor(private bucketService: BucketService,
private eventSourceService: EventSourceService,
private commonService: CommonService) {
}
initBucketWithProgress(id: number): void {
this.isDialogOpen = true;
this.progress = 0;
const handleCompletion = () => {
this.progress = 100;
setTimeout(() => {
this.isDialogOpen = false; // 关闭弹窗
this.commonService.success(() => {}, '初始化成功');
}, 1000);
};
this.eventSourceService.createEventSourceWithToken(`api/bucket/init/${id}`, 'PUT').subscribe(data => {
if (data === "complete") {
handleCompletion();
} else if (data) {
this.progress = data; // 更新进度
if (this.progress >= 100) {
handleCompletion();
}
}
});
}
HTML实现,这里使用angular
<div *ngIf="isDialogOpen" class="dialog">
<p class="dialog-title">初始化进度</p>
<mat-progress-bar class="progress-bar" mode="determinate" [value]="progress"></mat-progress-bar>
<p class="progress-text">{{ progress }}%</p>
</div>
最终实现效果
总结
如果想要前端返回进度最理想的就是使用SSE推流进行实现,但是eventSource有一个弊端就是不能自定义请求头,这点就很难受,没办法只能使用fetch api的方式进行实现,也能最终实现效果
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。