3

前言

当前有一个需求,当前需要依靠minio桶进行创建任务,而可能会出现一个问题就是504错误(Gateway Timeout),当前如果桶的任务数太大的话,后端服务器可能需要更长时间来完成请求,前台就迟迟得不到响应。

image.png

问题分析

这时候我们的解决思路就是希望后端处理的时候,返回一些进度信息,而前台接受到响应就不会出现504(Gatway Timeout),所以我们希望实现的效果就是使用sse的方式进行解决这个问题

实现案例

实现的效果

image.png

服务端实现

    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 表示还有数据。

image.png


    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>

最终实现效果

a.gif

总结

如果想要前端返回进度最理想的就是使用SSE推流进行实现,但是eventSource有一个弊端就是不能自定义请求头,这点就很难受,没办法只能使用fetch api的方式进行实现,也能最终实现效果


kexb
474 声望15 粉丝